| @@ -159,6 +159,10 @@ function PasteCredentialInputs(props: { · 158 unmodified lines |
| 159 | 159 | /** Force the per-input label even for a single input (env vars name their |
| 160 | 160 | * field rather than relying on a generic "value / token" placeholder). */ |
| 161 | 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 | 166 | readonly values: Readonly<Record<string, string>>; |
| 163 | 167 | readonly onChange: (values: Record<string, string>) => void; |
| 164 | 168 | }) { |
| @@ -178,7 +182,11 @@ function PasteCredentialInputs(props: { · 13 unmodified lines |
| 178 | 182 | <Input |
| 179 | 183 | id={`credential-input-${input.variable}`} |
| 180 | 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 | 190 | placeholder={`paste ${input.label}`} |
| 183 | 191 | value={props.values[input.variable] ?? ""} |
| 184 | 192 | onChange={(e: React.ChangeEvent<HTMLInputElement>) => |
| @@ -206,20 +214,55 @@ function PasteCredentialInputs(props: { · 21 unmodified lines |
| 206 | 214 | {labelled && ( |
| 207 | 215 | <Label className="font-mono text-xs text-muted-foreground">{input.label}</Label> |
| 208 | 216 | )} |
| 209 | | − <Input |
| 210 | | − type="password" |
| 211 | | − autoComplete="new-password" |
| 212 | | − placeholder={labelled ? "secret value" : "paste the value / token"} |
| 213 | | − value={props.values[input.variable] ?? ""} |
| 214 | | − onChange={(e: React.ChangeEvent<HTMLInputElement>) => |
| 215 | | − props.onChange({ |
| 216 | | − ...props.values, |
| 217 | | − [input.variable]: e.target.value, |
| 218 | | − }) |
| 219 | | − } |
| 220 | | − className="font-mono" |
| 221 | | − data-ph-block |
| 222 | | − /> |
| 217 | + {props.affix ? ( |
| 218 | + // Merged field: the placement's lead + prefix is a FIXED addon (its |
| 219 | + // own muted segment, divider, non-selectable), and the user types |
| 220 | + // only the token after it — the field reads as the header value being |
| 221 | + // built. Only the border reacts to focus (no full-field glow). The |
| 222 | + // autoComplete/ignore attrs stop the browser and password managers |
| 223 | + // from offering to GENERATE a password here; the app's own 1Password |
| 224 | + // picker covers filling an existing secret. |
| 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"> |
| 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"> |
| 227 | + {props.affix} |
| 228 | + </span> |
| 229 | + {/* oxlint-disable-next-line react/forbid-elements */} |
| 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 | 266 | </div> |
| 224 | 267 | ))} |
| 225 | 268 | </div> |
| @@ -297,6 +340,8 @@ function CredentialValueFields(props: { · 71 unmodified lines |
| 297 | 340 | readonly inputs: readonly CredentialInput[]; |
| 298 | 341 | readonly singleInput: boolean; |
| 299 | 342 | readonly showLabels?: boolean; |
| 343 | + /** Single-input only: placement lead + prefix to merge into the field. */ |
| 344 | + readonly affix?: string | null; |
| 300 | 345 | /** Allow an external provider (1Password) origin. Off for env methods: the |
| 301 | 346 | * wire's external `from` is keyed under the canonical `token` variable, but |
| 302 | 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 | 396 | inputs={props.inputs} |
| 352 | 397 | singleInput={props.singleInput} |
| 353 | 398 | showLabels={props.showLabels} |
| 399 | + affix={props.affix} |
| 354 | 400 | values={props.values} |
| 355 | 401 | onChange={props.onValuesChange} |
| 356 | 402 | /> |
| @@ -1002,6 +1048,20 @@ function AddAccountModalView(props: AddAccountModalProps) { · 645 unmodified lines |
| 1002 | 1048 | // registration entirely. |
| 1003 | 1049 | const isCimd = isOAuth && method?.oauth?.supportsClientIdMetadataDocument === true; |
| 1004 | 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 | 1065 | // DCR-capable: the integration advertises dynamic registration (MCP oauth2), |
| 1006 | 1066 | // OR carries a discovery URL we can probe at connect time. When DCR-capable |
| 1007 | 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 | 1667 | <Tabs |
| 1608 | 1668 | value={methodId} |
| 1609 | 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 | 1673 | <div className="flex w-max shrink-0 items-stretch gap-1"> |
| 1614 | 1674 | {allMethods.map((m: AuthMethod) => ( |
| 1615 | 1675 | <div |
| @@ -1669,10 +1729,14 @@ function AddAccountModalView(props: AddAccountModalProps) { · 53 unmodified lines |
| 1669 | 1729 | "mt-0 min-w-0 space-y-5", |
| 1670 | 1730 | // No-auth renders no fields, so skip the framed box that |
| 1671 | 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 | 1741 | const shown = method.placements.filter((p) => p.carrier !== "env"); |
| 1678 | 1742 | return shown.length > 0 ? ( |
| @@ -1818,6 +1882,7 @@ function AddAccountModalView(props: AddAccountModalProps) { · 139 unmodified lines |
| 1818 | 1882 | inputs={credentialInputs} |
| 1819 | 1883 | singleInput={singleInput} |
| 1820 | 1884 | showLabels={isEnvMethod} |
| 1885 | + affix={singleCredentialAffix} |
| 1821 | 1886 | allowExternalProvider={!isEnvMethod} |
| 1822 | 1887 | values={values} |
| 1823 | 1888 | onValuesChange={setValues} |