| @@ -159,6 +159,10 @@ function PasteCredentialInputs(props: { · 158 unmodified lines |
| 159 | /** Force the per-input label even for a single input (env vars name their | 159 | /** Force the per-input label even for a single input (env vars name their |
| 160 | * field rather than relying on a generic "value / token" placeholder). */ | 160 | * field rather than relying on a generic "value / token" placeholder). */ |
| 161 | readonly showLabels?: boolean; | 161 | readonly showLabels?: boolean; |
| | 162 | /** Single-input only: the placement's lead + prefix (e.g. "Authorization: |
| | 163 | * Bearer "), rendered as a non-editable affix INSIDE the field so the input |
| | 164 | * reads as the header value being built. Replaces the separate preview line. */ |
| | 165 | readonly affix?: string | null; |
| 162 | readonly values: Readonly<Record<string, string>>; | 166 | readonly values: Readonly<Record<string, string>>; |
| 163 | readonly onChange: (values: Record<string, string>) => void; | 167 | readonly onChange: (values: Record<string, string>) => void; |
| 164 | }) { | 168 | }) { |
| @@ -178,7 +182,11 @@ function PasteCredentialInputs(props: { · 13 unmodified lines |
| 178 | <Input | 182 | <Input |
| 179 | id={`credential-input-${input.variable}`} | 183 | id={`credential-input-${input.variable}`} |
| 180 | type="password" | 184 | type="password" |
| 181 | autoComplete="new-password" | 185 | autoComplete="off" |
| | 186 | data-1p-ignore |
| | 187 | data-lpignore="true" |
| | 188 | data-bwignore |
| | 189 | data-form-type="other" |
| 182 | placeholder={`paste ${input.label}`} | 190 | placeholder={`paste ${input.label}`} |
| 183 | value={props.values[input.variable] ?? ""} | 191 | value={props.values[input.variable] ?? ""} |
| 184 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => | 192 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => |
| @@ -206,20 +214,55 @@ function PasteCredentialInputs(props: { · 21 unmodified lines |
| 206 | {labelled && ( | 214 | {labelled && ( |
| 207 | <Label className="font-mono text-xs text-muted-foreground">{input.label}</Label> | 215 | <Label className="font-mono text-xs text-muted-foreground">{input.label}</Label> |
| 208 | )} | 216 | )} |
| 209 | <Input | 217 | {props.affix ? ( |
| 210 | type="password" | 218 | // Merged field: the placement's lead + prefix is a FIXED addon (its |
| 211 | autoComplete="new-password" | 219 | // own muted segment, divider, non-selectable), and the user types |
| 212 | placeholder={labelled ? "secret value" : "paste the value / token"} | 220 | // only the token after it — the field reads as the header value being |
| 213 | value={props.values[input.variable] ?? ""} | 221 | // built. Only the border reacts to focus (no full-field glow). The |
| 214 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => | 222 | // autoComplete/ignore attrs stop the browser and password managers |
| 215 | props.onChange({ | 223 | // from offering to GENERATE a password here; the app's own 1Password |
| 216 | ...props.values, | 224 | // picker covers filling an existing secret. |
| 217 | [input.variable]: e.target.value, | 225 | <div className="flex h-9 w-full min-w-0 items-stretch overflow-hidden rounded-md border border-input bg-transparent font-mono text-sm shadow-xs transition-colors focus-within:border-ring dark:bg-input/30"> |
| 218 | }) | 226 | <span className="flex shrink-0 select-none items-center whitespace-pre border-r border-input bg-muted/40 px-3 text-muted-foreground/70"> |
| 219 | } | 227 | {props.affix} |
| 220 | className="font-mono" | 228 | </span> |
| 221 | data-ph-block | 229 | {/* oxlint-disable-next-line react/forbid-elements */} |
| 222 | /> | 230 | <input |
| | 231 | type="password" |
| | 232 | autoComplete="off" |
| | 233 | data-1p-ignore |
| | 234 | data-lpignore="true" |
| | 235 | data-bwignore |
| | 236 | data-form-type="other" |
| | 237 | placeholder="token" |
| | 238 | value={props.values[input.variable] ?? ""} |
| | 239 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => |
| | 240 | props.onChange({ ...props.values, [input.variable]: e.target.value }) |
| | 241 | } |
| | 242 | className="min-w-0 flex-1 bg-transparent px-3 outline-none placeholder:text-muted-foreground/60" |
| | 243 | data-ph-block |
| | 244 | /> |
| | 245 | </div> |
| | 246 | ) : ( |
| | 247 | <Input |
| | 248 | type="password" |
| | 249 | autoComplete="off" |
| | 250 | data-1p-ignore |
| | 251 | data-lpignore="true" |
| | 252 | data-bwignore |
| | 253 | data-form-type="other" |
| | 254 | placeholder={labelled ? "secret value" : "paste the value / token"} |
| | 255 | value={props.values[input.variable] ?? ""} |
| | 256 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => |
| | 257 | props.onChange({ |
| | 258 | ...props.values, |
| | 259 | [input.variable]: e.target.value, |
| | 260 | }) |
| | 261 | } |
| | 262 | className="font-mono" |
| | 263 | data-ph-block |
| | 264 | /> |
| | 265 | )} |
| 223 | </div> | 266 | </div> |
| 224 | ))} | 267 | ))} |
| 225 | </div> | 268 | </div> |
| @@ -297,6 +340,8 @@ function CredentialValueFields(props: { · 71 unmodified lines |
| 297 | readonly inputs: readonly CredentialInput[]; | 340 | readonly inputs: readonly CredentialInput[]; |
| 298 | readonly singleInput: boolean; | 341 | readonly singleInput: boolean; |
| 299 | readonly showLabels?: boolean; | 342 | readonly showLabels?: boolean; |
| | 343 | /** Single-input only: placement lead + prefix to merge into the field. */ |
| | 344 | readonly affix?: string | null; |
| 300 | /** Allow an external provider (1Password) origin. Off for env methods: the | 345 | /** Allow an external provider (1Password) origin. Off for env methods: the |
| 301 | * wire's external `from` is keyed under the canonical `token` variable, but | 346 | * wire's external `from` is keyed under the canonical `token` variable, but |
| 302 | * an env credential must be keyed under its env var name, so a 1Password | 347 | * an env credential must be keyed under its env var name, so a 1Password |
| @@ -351,6 +396,7 @@ function CredentialValueFields(props: { · 48 unmodified lines |
| 351 | inputs={props.inputs} | 396 | inputs={props.inputs} |
| 352 | singleInput={props.singleInput} | 397 | singleInput={props.singleInput} |
| 353 | showLabels={props.showLabels} | 398 | showLabels={props.showLabels} |
| | 399 | affix={props.affix} |
| 354 | values={props.values} | 400 | values={props.values} |
| 355 | onChange={props.onValuesChange} | 401 | onChange={props.onValuesChange} |
| 356 | /> | 402 | /> |
| @@ -1002,6 +1048,20 @@ function AddAccountModalView(props: AddAccountModalProps) { · 645 unmodified lines |
| 1002 | // registration entirely. | 1048 | // registration entirely. |
| 1003 | const isCimd = isOAuth && method?.oauth?.supportsClientIdMetadataDocument === true; | 1049 | const isCimd = isOAuth && method?.oauth?.supportsClientIdMetadataDocument === true; |
| 1004 | const cimdActive = isCimd; | 1050 | const cimdActive = isCimd; |
| | 1051 | // Single-input header/query methods: the placement's lead + prefix (e.g. |
| | 1052 | // "Authorization: Bearer ") merges INTO the credential field as a non-editable |
| | 1053 | // affix, so the input reads as the header value being built and the separate |
| | 1054 | // preview line is dropped. Env and multi-input methods keep the plain field. |
| | 1055 | const singleCredentialAffix = useMemo<string | null>(() => { |
| | 1056 | if (!method || !singleInput || isEnvMethod) return null; |
| | 1057 | const placement = method.placements.find((p) => p.carrier !== "env"); |
| | 1058 | if (!placement) return null; |
| | 1059 | const lead = |
| | 1060 | placement.carrier === "header" |
| | 1061 | ? `${placement.name || "Authorization"}: ` |
| | 1062 | : `?${placement.name || "api_key"}=`; |
| | 1063 | return `${lead}${placement.prefix ?? ""}`; |
| | 1064 | }, [method, singleInput, isEnvMethod]); |
| 1005 | // DCR-capable: the integration advertises dynamic registration (MCP oauth2), | 1065 | // DCR-capable: the integration advertises dynamic registration (MCP oauth2), |
| 1006 | // OR carries a discovery URL we can probe at connect time. When DCR-capable | 1066 | // OR carries a discovery URL we can probe at connect time. When DCR-capable |
| 1007 | // and not yet fallen back, we skip the app picker entirely (Option A). | 1067 | // and not yet fallen back, we skip the app picker entirely (Option A). |
| @@ -1607,9 +1667,9 @@ function AddAccountModalView(props: AddAccountModalProps) { · 599 unmodified lines |
| 1607 | <Tabs | 1667 | <Tabs |
| 1608 | value={methodId} | 1668 | value={methodId} |
| 1609 | onValueChange={selectMethod} | 1669 | onValueChange={selectMethod} |
| 1610 | className="w-full min-w-0 max-w-full gap-3" | 1670 | className="w-full min-w-0 max-w-full gap-0" |
| 1611 | > | 1671 | > |
| 1612 | <TabsList className="flex h-10 w-fit min-w-0 max-w-full justify-start overflow-x-auto overflow-y-hidden p-1 [scrollbar-width:thin]"> | 1672 | <TabsList className="flex h-10 w-full min-w-0 max-w-full justify-start overflow-x-auto overflow-y-hidden rounded-b-none rounded-t-md border border-b-0 border-border/60 bg-muted/30 p-1 [scrollbar-width:thin]"> |
| 1613 | <div className="flex w-max shrink-0 items-stretch gap-1"> | 1673 | <div className="flex w-max shrink-0 items-stretch gap-1"> |
| 1614 | {allMethods.map((m: AuthMethod) => ( | 1674 | {allMethods.map((m: AuthMethod) => ( |
| 1615 | <div | 1675 | <div |
| @@ -1669,10 +1729,14 @@ function AddAccountModalView(props: AddAccountModalProps) { · 53 unmodified lines |
| 1669 | "mt-0 min-w-0 space-y-5", | 1729 | "mt-0 min-w-0 space-y-5", |
| 1670 | // No-auth renders no fields, so skip the framed box that | 1730 | // No-auth renders no fields, so skip the framed box that |
| 1671 | // would otherwise show up as an empty bordered panel. | 1731 | // would otherwise show up as an empty bordered panel. |
| 1672 | isNoAuth ? null : "rounded-md border border-border/60 bg-muted/15 p-4", | 1732 | // Otherwise the panel attaches to the tab header above |
| | 1733 | // (square top, rounded bottom). |
| | 1734 | isNoAuth |
| | 1735 | ? null |
| | 1736 | : "rounded-b-md rounded-t-none border border-border/60 bg-muted/15 p-4", |
| 1673 | )} | 1737 | )} |
| 1674 | > | 1738 | > |
| 1675 | {method?.placements && !isEnvMethod && singleInput | 1739 | {method?.placements && !isEnvMethod && singleInput && !singleCredentialAffix |
| 1676 | ? (() => { | 1740 | ? (() => { |
| 1677 | const shown = method.placements.filter((p) => p.carrier !== "env"); | 1741 | const shown = method.placements.filter((p) => p.carrier !== "env"); |
| 1678 | return shown.length > 0 ? ( | 1742 | return shown.length > 0 ? ( |
| @@ -1818,6 +1882,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 139 unmodified lines |
| 1818 | inputs={credentialInputs} | 1882 | inputs={credentialInputs} |
| 1819 | singleInput={singleInput} | 1883 | singleInput={singleInput} |
| 1820 | showLabels={isEnvMethod} | 1884 | showLabels={isEnvMethod} |
| | 1885 | affix={singleCredentialAffix} |
| 1821 | allowExternalProvider={!isEnvMethod} | 1886 | allowExternalProvider={!isEnvMethod} |
| 1822 | values={values} | 1887 | values={values} |
| 1823 | onValuesChange={setValues} | 1888 | onValuesChange={setValues} |