TUI Design

Component Reuse & Catalog

The hard reuse rule, the full reusable component catalog, and shared interaction patterns for building jackin❯ TUI features.

Component Reuse — Hard Rule

Never copy-paste a TUI component. Extend or compose instead. This is the single most important architectural rule for the jackin TUI.

Core rules

  1. Every visual pattern that appears in more than one place must use one shared implementation. A single widget, render function, or helper — not two near-copies. When the existing implementation cannot serve a new call site without modification, extend it (add a parameter, generalize a branch, add an enum variant) rather than forking it.

  2. Components have two canonical homes. Product-neutral components live in TermRock. Product composition and policy live in crates/<surface-crate>/src/tui/components/. Console service adapters bind component outcomes to root IO.

  3. A genuinely new component is only ever written once. The first time a new UX pattern appears, it is acceptable to implement it in the most focused way that serves the immediate need. The second time the same pattern is needed somewhere else, the two implementations must be consolidated into one shared widget before the second PR lands — not "as a follow-up."

  4. Copy-paste for convenience is a blocker. When a PR produces a second near-copy of an existing widget or render function — even 90% similar, even with minor additions — that is a TUI design-decision violation. Reviewers block merge; the fix is consolidation, not a comment.

  5. Refactoring to enable reuse is not optional. If an existing component does not yet accept a parameter that a new call site needs, add the parameter. If an existing component mixes two responsibilities that need to be separated, separate them. The cost of a targeted refactor is always lower than the cost of maintaining two divergent implementations through every future bug fix and enhancement.

  6. Settings screens mirror workspace screens. Settings surfaces that intentionally parallel workspace editor screens must reuse the workspace widgets and flow helpers wherever behavior is the same; keep separate code only for the different persistence target or config scope. Visual drift between the two is a bug.

  7. Component APIs follow the TUI boundary. Component props/state/messages may describe UI state and emit semantic outcomes. They must not own Docker/git/op/gh calls, config persistence, workspace resolution, or protocol authority. When a component needs work done, it emits an outcome/message that update turns into a typed effect.

Component API contract

Shared TermRock components use one of three API shapes:

  1. Stateful interactive widgetpub struct XState<Id> owns UI state and stable identities; the component implements Ratatui StatefulWidget and returns typed semantic outcomes from input methods. Rendering may update measured viewport, regions, and clamped scroll state, but effects remain with the consumer. There is one public rendering body, not a parallel free renderer.
  2. Pure value widget — display-only values implement Ratatui Widget. A convenience function is acceptable only when it delegates to the widget instead of carrying a second render body.
  3. Layout and primitive helpers — rectangle math, scrollbars, blocks, hit testing, and backdrop helpers stay as free functions or small helper types. They do not introduce widget wrappers unless they render a concrete reusable value.

Input methods return typed component outcomes (ListOutcome, DetailTableOutcome, ModalOutcome, or the widget's equivalent). Mouse and scroll input use separately named methods so keyboard, pointer, and render geometry share the same state and stable IDs.

What to check before writing new code

Before adding any new TUI widget or state type:

  1. Search the pinned TermRock source and catalog first.
  2. Search the owning surface's final local component area: rg 'struct .*State|fn render_|impl Widget|impl StatefulWidget' crates/<surface-crate>/src/tui.
  3. Search the owning surface's adapter shell before moving existing behavior: rg 'struct .*State|fn render_' crates/jackin/src/console crates/jackin-console/src/tui.
  4. If a component already owns the pattern, extend it. Add a mode enum, parameter, flag, or semantic variant. Do not create a second module.
  5. If no component owns the pattern and only one surface needs it, add it under crates/<surface-crate>/src/tui/components/ with a thin components.rs export. Product-neutral multi-consumer patterns belong upstream in TermRock with catalog and visual-regression coverage.
  6. When moving an existing component into the final structure, identify existing tests/snapshots or add focused coverage, move the code, delete the superseded implementation and compatibility facade, and verify keyboard, mouse, focus, footer, error, and render behavior afterward. This pre-release codebase does not retain migration shims.

Why this rule exists — canonical lesson

A token_store_picker widget once duplicated the Account → Vault → Item navigation state machine from crates/jackin-console/src/tui/op_picker.rs — a 1067-line copy of closely parallel code where every op_picker bug fix to navigation, filtering, R-key refresh, or section display had to be manually reproduced or the two implementations drifted. That copy has since been deleted and op_picker was extended instead: an opt-in OpPickerMode::Create turns on the + New item, + New field, and + New section creation sentinels and the naming sub-stages, and the picker commits an OpPickerSelection enum (Existing / NewItem / EditItemField). One widget now serves both the browse and create call sites. This is the positive reference for the rule: when a new call site needs more, extend the shared widget with a mode, never fork it into a second drill-down. The remaining Claude-token work is tracked in the Workspace Claude Token Setup roadmap item.


Reusable Component Catalog

Reusable neutral TUI widgets live first in TermRock. Product-local widgets live under the owning surface's src/tui/components/. Console adapter modules may own runner wiring, cache binding, and service execution, but they are not homes for new canonical component patterns. Use this table to identify the right component before writing new code or moving an existing widget.

ComponentModuleWhat it providesWhen to use
Op picker (1Password drill-down)crates/jackin-console/src/tui/op_picker.rsAccount → Vault → Item → Field 4-stage drill-down with filter, R-key refresh, collapsible section headers, background loading, and op:// reference commit. An opt-in OpPickerMode::Create adds + New item / + New field / + New section creation sentinels and naming sub-stages, committing an OpPickerSelection enumAny flow that needs to browse, pick, or create a 1Password field reference
Role pickercrates/jackin-console/src/tui/components/role_picker.rsFilter list of available roles with type-to-narrow and Enter commitAny flow that needs to select a role from the known set
Text input boxTermRock sourceSingle-line labelled input box (TextInputState + render_text_input), shared prompt geometry (text_input_prompt_rect), and the titled/labelled variant (render_labeled_text_input_dialog) with validation, duplicate detection, forbidden-char rules. This is the "Credential" dialog the operator sees when typing any single value.Any flow needing one line of free text — env value, item name, field label, section name, workspace name
Scope pickercrates/jackin-console/src/tui/components/scope_picker.rsTwo-button horizontal strip: All roles / Specific roleAny "apply to everything vs apply to one" choice
Source pickercrates/jackin-console/src/tui/components/source_picker.rsTwo-button horizontal strip: Plain text / 1PasswordChoosing whether a value is literal or an op:// reference
Agent choicecrates/jackin-console/src/tui/components/agent_choice.rsVertical picker over a variable-size choices: Vec<A> agent setPicking one agent from the available agent choices
Mount destination choicecrates/jackin-console/src/tui/components/mount_dst_choice.rsTwo-button strip for mount destination choicesMount configuration flows
Workdir pickercrates/jackin-console/src/tui/components/workdir_pick.rsFiltered list of mounted workspace pathsMount configuration flows that reuse an existing workspace path
Brand headerconsole brand header + launch header compositionProduct-owned brand pill row with the current surface label; the literal jackin wordmark does not belong in TermRockHeader chrome that needs the canonical jackin brand mark
Button stripTermRock sourceShared centered button row with focused and disabled item stylingDialogs and form footers with multiple inline actions
Confirm dialogTermRock sourceYes/No two-button confirm with the canonical layout (see Confirmation dialogs rule), plus generic structured detail rows and note rows for prompts that need extra context. Product-specific strings are assembled by the owning surface, not by TermRock.Any destructive or irreversible action
Debug info compositionproduct facade + TermRock DetailTableOne jackin fact vocabulary and canonical row builder projected into TermRock's neutral dialog panel, table, scrolling, copy/link regions, hover, and copied-state renderingDebug info dialogs across console, launch, and Capsule surfaces
Diff viewTermRock sourceRead-only side-by-side or single-pane text diff view backed by similar::TextDiffInspecting modified, added, deleted, or untracked file content
Filter inputTermRock sourceCanonical single-row Filter: input with placeholder and cursor stylingPicker dialogs that narrow a list by typed text
Hint barTermRock sourceFooter hint-bar renderer for grouped HintSpan shortcuts, including wrapped layoutFixed footer rows that advertise active keyboard and mouse actions
PanelTermRock sourceShared bordered panel primitive with canonical focused, scrollable, and modal block stylingAny framed TUI panel or modal shell
Save / discard stripTermRock sourceSave / Discard two-button strip, designed for form footersBottom of any editable form
Scrollable panelTermRock sourceShared scroll geometry, scrollable panels, line rendering, and vertical/horizontal scrollbar drawingAny panel, dialog, or list body that can overflow
Select listTermRock sourceGeneric modal filter-picker over labelled string items plus full-width picker row renderingModal lists, sectioned pickers, and simple filterable selections
Status barTermRock StatusBarCanonical responsive slots, priority, truncation, fade, hover, and hit regions; jackin owns the meaning and ordering of product slotsLaunch, console, or Capsule bottom chrome that reports current run state
Status popupTermRock sourceGeneric non-interactive modal status popup with a message and Please wait action rowBackground work progress that blocks interaction until completion
Tab stripTermRock sourceShared Ratatui tab strip with active, inactive, focused, hover, and hit-test behaviorSettings/editor tabs and any equivalent tabbed TUI surface
ToastTermRock sourceSemantic severity, anchored placement, margins, narrow clamping, and rendering; the surface owns message and expiryCopy-success or similar transient feedback that must appear outside the hint/footer row
File browsercrates/jackin-console/src/tui/components/file_browser.rs + crates/jackin-console/src/services/file_browser.rsHost filesystem navigation UI; navigation, commit validation, and Git URL lookup leave as typed outcomes for non-TUI adapters; browser-open queues ManagerEffect::OpenUrl for the root effect executorAny flow needing to pick a local file or directory
GitHub pickercrates/jackin-console/src/tui/components/github_picker.rsGitHub org/repo drill-downAny flow needing to select a GitHub repository
Error popupTermRock sourceScrollable red-border error modal (see Error Surface rule)All error display

Layout and interaction primitives that support the catalogued widgets live in TermRock source, TermRock source, TermRock source, TermRock source, TermRock source, and TermRock source; surface-local decorative widgets such as launch rain in crates/jackin-launch/src/tui/components/rain.rs are intentionally not catalogued as shared components.

Console role-load error text is assembled through crates/jackin-console/src/tui/components/error_popup.rs. Root state may classify root-only repository and validation error types, but popup wording belongs to the console TUI component boundary.

Console effect executors use the same error-popup helper boundary for side-effect failure titles such as token generation, URL opening, workspace deletion, and file-browser startup. The executor owns the work and the concrete error value; crates/jackin-console/src/tui/components/error_popup.rs owns the visible popup title/state construction.

Background-work status popups follow the same split: crates/jackin/src/console/effects.rs starts role registration, workspace drift checks, and isolated-state cleanup, while crates/jackin-console/src/tui/components/status_popup.rs owns the visible status title/message construction.

Root-console tests should seed modals through the same crate-owned helpers as runtime whenever the helper exists. This keeps the final-structure audit honest: direct constructors in root tests are treated as drift unless the test is explicitly exercising the shared primitive itself.

Test-only helpers that need command runners, service calls, or config persistence belong with effect/service adapters, not input handlers. Role-input tests may use fake runners through crates/jackin/src/console/effects.rs, but crates/jackin-console/src/tui/input/editor.rs must stay limited to input-to-outcome routing and modal state transitions.

The console op-picker adapter may own operator_env types, runner wiring, cache binding, subscriptions, and crossterm-to-outcome mapping, but user-visible picker copy, load-error state shaping, and blocked-load key policy belong in crates/jackin-console/src/tui/components/op_picker.rs with the rest of the component. Root adapters pass concrete process errors, subscription-closed facts, or Esc/not-Esc key facts into component helpers; they do not construct recoverable/fatal picker error states inline or duplicate the loading/fatal key-consumption rule.

Editor save prompts and save-blocking error text that depend only on visible editor facts live with editor screen view helpers in crates/jackin-console/src/tui/screens/editor/view.rs. Root save input and the effect executor still own drift detection, preserved-state cleanup, and workspace writes; they pass render-safe facts such as affected container names into the screen helpers.

Editor General-tab row vocabulary and content-width planning also live with editor screen view helpers in crates/jackin-console/src/tui/screens/editor/view.rs. Root layout code may shorten paths and pass current booleans, but it must not duplicate the row labels or enabled/disabled wording used by rendering.

Editor header title wording lives with editor screen view helpers in crates/jackin-console/src/tui/screens/editor/view.rs. Root rendering supplies only whether the editor is creating or editing and, for edit mode, the workspace name.

Settings header title wording lives with settings screen view helpers in crates/jackin-console/src/tui/screens/settings/view.rs. Root rendering should ask that helper for the screen title rather than carrying settings-screen copy.

Workspace-list header title wording lives with top-level console view helpers in crates/jackin-console/src/tui/view.rs. Root frame rendering should ask that helper for the workspace screen title rather than carrying workspace-screen copy.

Editor/settings save-footer base labels and pick-list modal commit labels live with console-local footer hint components in crates/jackin-console/src/tui/components/footer_hints.rs. Root footer adapters may pass dirty/change facts, row modes, and modal variants, but they should not carry the save workspace, save settings, select, or confirm copy.

Generated-token source-picker label construction lives with the auth-panel component in crates/jackin-console/src/tui/components/auth_panel.rs. Editor and settings input code decides when token generation starts, but visible source-picker state comes from the component helper.

Generated-token 1Password item-name default construction also lives with the auth-panel component in crates/jackin-console/src/tui/components/auth_panel.rs. Editor and settings token-generation input code should pass the template and scope label into the helper rather than hard-coding {ws} replacement in root input handlers.

Lookbook focus behavior

The external TermRock lookbook follows the same focus model as every other jackin surface:

  • Sidebar / story list — focus starts here on launch; the sidebar panel shows a PHOSPHOR_GREEN border; / navigate stories; Tab or a click on the preview panel transfers focus to the preview.
  • Preview panel — shows a PHOSPHOR_GREEN border when focused; Esc/Tab/ returns focus to the sidebar; keyboard events route to the active component interactor inside the preview.

The external lookbook is the visual regression surface for every neutral component in the catalog. Product-local compositions remain covered by their owning crate's render tests.

Render primitives: TermRock Panel / render_dialog_shell / Viewport / DialogScroll (TermRock source), console scroll_block and dialog_layout adapters when call shape needs them, breadcrumb_title (crates/jackin-console/src/tui/components/op_picker.rs), render_fatal (crates/jackin-console/src/tui/components/op_picker.rs), TermRock filter input and list widgets — use these building blocks when composing new modals rather than hand-rolling border + layout from scratch.

One picker-list renderer, with full-width section dividers

Every modal list — the host console pickers and the in-container capsule menu/pickers — draws its rows through the shared picker renderer in select_list (TermRock source). Full modal pickers use render_picker_list with PickerRow values; richer host pickers that own their own filter/state flow must still pass neutral, unselected Line rows to render_picker_lines. Callers must not pre-style selection, paint their own cursor, or stop the selected background at the text width. The shared renderer owns the selected-row gutter, full-width highlight, selection-follow viewport, and the rule that scrollbar gutter cells win over highlight cells.

There are exactly two full-picker row kinds, and a caller must not hand-roll either:

  • PickerRow::Item — a selectable row. The selected row gets the canonical highlight (PHOSPHOR_GREEN background, black bold text, cursor) in a reserved 2-column gutter, so item labels align whether or not they are selected.
  • PickerRow::Separator(label) — a non-selectable ──── label ──── section divider. A section divider must span the full inner width, border to border, with the label centered. render_picker_list repaints separator rows edge-to-edge over the selection gutter so the dashes reach both borders symmetrically — the divider is the one row that deliberately ignores the gutter. Dashes are PHOSPHOR_DARK, the label is dim. A divider that is indented on one side (gutter-shifted), shorter than the inner width, or whose label is not centered is a violation. Pass the bare label text ("agents", "shells") — never pre-build the dashes at the call site, or the width math drifts from the gutter.

One scrollbar renderer

Every scrollbar in jackin — host console panels, dialogs, and capsule pane scrollback — must render through termrock::scroll or a canonical TermRock widget. A hand-painted thumb loop, custom glyph, or per-surface color is a review-blocking violation. Tail-relative surfaces bridge to TermRock top-relative offsets with termrock::scroll::TailScroll::to_top_offset and use the shared track-position mapping so click-to-jump and the painted thumb share one geometry.

Two-button choice pattern

scope_picker.rs, source_picker.rs, mount_dst_choice.rs, and confirm.rs are all the same visual shape: a small modal with two side-by-side buttons, ←/→ to move, Enter to select, Esc to cancel. When a step requires the operator to pick one of exactly two mutually-exclusive options, reuse one of these or model a new one on the identical shape — never invent a third layout. The operator must recognise "this is a two-way choice" instantly from the shape alone.

One input-box dialog for every single-value prompt

Every "ask the operator to type one value" prompt MUST use the shared text_input dialog (TermRock sourceTextInputState + render_text_input, or render_labeled_text_input_dialog when the dialog title and field label are distinct). That render fn draws the whole dialog: a single bordered box, the label/title, the input row, the dim input band, and the cursor styling. It is the box behind "New global environment key", the credential entry, the launch credential prompt, the workspace-name prompt, capsule Rename tab, and the op_picker Create-mode naming stages. Do not hand-roll a second input box, and do not wrap text_input::render_text_input inside another bordered block (that produces a box-inside-a-box). The dialog is a single box sized by the shared text-input rect helper for that surface, with the footer showing the active commit/cancel hints — supplied by the modal footer, never drawn inside the box (see "Hints: footer only"). When a flow that already owns a larger modal (like the op_picker drill-down) needs a one-value sub-prompt, render the plain text_input box at the text-input rect for that sub-stage rather than nesting it in the parent frame.

Default values are pre-filled in input boxes

When an input box has a suggested default, that default MUST be pre-filled into the box (cursor at end), not shown as placeholder/ghost text. The operator either presses Enter to accept the pre-filled value or edits it. Never make the operator type a value that the system already knows the default for. This matches the CLI prompt convention (dialoguer::Input::default(...), where Enter accepts the suggestion) so the two surfaces behave identically. Construct the input with the default as its initial content — TextInputState::new(label, default) already does this (the textarea opens holding default with the cursor at the end). Use an empty initial value ("") only when there is genuinely no sensible default (e.g. a new section name). Example: the op_picker Create-mode naming stages open with Claude (item) and oauth-token (field) pre-filled; the operator hits Enter to take them or types over them.

Toasts are state feedback, never action hints

Transient state feedback such as Selection copied uses TermRock's canonical Toast, including severity styling and anchored placement above caller-supplied reserved footer/status rows. The owning surface retains the message and expiry timer. A toast is non-focusable. Do not put copy-success or other state feedback in the hint/footer row: that row is reserved for actions currently available in the focused area.

Creation-sentinel pattern (+ Add X / + New X)

jackin has one convention for "add a new thing to this list": a sentinel row rendered at the bottom of the list with a leading + and the action label. It is the same across every surface:

Selecting a creation sentinel opens the next step in the flow — usually the text input box to name the new thing, or a two-button choice. Any new "add an item to this list" affordance MUST render as a + sentinel row in the same position and style, and MUST route its selection through the text input box (for naming) or a picker (for choosing). Do not invent a different add-affordance (no separate "New…" button floating elsewhere, no key-only shortcut without a visible row).


Wizard-Style Dialog Flows

When a feature requires the operator to complete several sequential decisions before committing (pick a scope, pick a role, pick a source, select a credential location), implement it as a wizard flow: a chain of modals where each step is handled by a reusable component and Esc walks back one step.

Rules

  • Each step is one reusable component from the catalog above. Do not design a custom combined-step UI.
  • Modal stack discipline applies (see Sub-dialogs push onto a stack). Each step pushes onto the modal stack; Esc pops one step; a terminal commit clears the whole chain.
  • The first step in the chain is opened from whatever UI element the operator interacts with (a button, a hint, an auth-tab action). It does not require a CLI flag.
  • The flow lives in the TUI, not in the CLI. The CLI is for scripted or non-interactive invocations. The TUI is for interactive flows.

Canonical example — Claude token setup flow

This is the reference example for how to design any multi-step TUI flow in jackin, and it is shipped. Every screen reuses a component from the catalog above; the flow adds zero new widgets and only extends op_picker (OpPickerMode::Create). Read this whenever you design a new flow and ask "which existing component is each of my screens?" before writing any code.

The flow configures a Claude OAuth token, launched entirely from inside jackin console — the operator never leaves the console and never runs a CLI command.

Scope comes from the auth form, not a picker step. The operator chooses what the token wires by opening the right Claude oauth_token auth form: the workspace-level form (all roles), a per-role override form (reached via + Override for a role), or the global form in Settings. There is deliberately no scope/role picker inside the generate flow — the auth tab's existing structure is the scope chooser, so the flow needs no scope_picker/role_picker step.

Entry point — G on the Edit auth dialog. With the form on Mode: oauth_token, G (shown in the footer as G generate) starts the flow. The screens, each mapped to the component it reuses:

StepScreenComponent reusedNotes
1Source: Plain text / 1PasswordSource picker (source_picker.rs)The same two-button dialog the provide flow uses.
2a (if "Plain text")(no screen)The token is minted by claude setup-token and stored as a literal at the scope; nothing is typed.
2b (if "1Password")Account → Vault → Item → [Section →] Field drill-downop_picker (Create mode)The existing op_picker, reused for the drill-down; OpPickerMode::Create adds the Section stage + creation sentinels below.

After the source/location choice, the console suspends, runs the claude setup-token OAuth capture, writes the minted token, and resumes — the storage location was already chosen, so the mint is the only blocking step.

Step 2b in detail — extending op_picker, not copying it. The 1Password branch reuses the existing op_picker Account, Vault, and Item screens exactly as they render today. Create mode adds a Section stage for an existing item and creation sentinels, each following the + Add X pattern (a + row at the bottom of the list):

  • Item stage — a + New item sentinel below the item list. Selecting it opens the text input box to name the new item, then names the field — the token is written there.
  • Section stage (existing item only) — rows are (root) (unsectioned fields), each existing named section, and a + New section sentinel. Picking a section scopes the next screen; + New section opens the text input box to name it.
  • Field stage (scoped to the chosen section) — the existing fields in that section, plus a + New field sentinel that opens the text input box to name the field.
  • Selecting an existing field writes the token into that field in-place (overwrite); creation rows write into the chosen section ((root) ⇒ unsectioned). Browse mode skips the Section stage and keeps the flat field list with collapsible section headers.

The root console run loop that drives the wizard lives under crates/jackin/src/console/adapter/run.rs; it suspends and resumes the terminal while external minting and write validation run through non-TUI services.

Every "create" affordance in this branch is the same + sentinel row, and every "name it" prompt is the same text input box. Nothing new is drawn.

Implementation contract. These sentinels are shipped in op_picker behind the opt-in OpPickerMode::Create mode parameter that turns the creation rows on; the picker commits an OpPickerSelection enum (Existing / NewItem / EditItemField) instead of only the plain op:// reference string. This capability now lives in the shared crates/jackin-console/src/tui/op_picker.rs — it MUST NOT be reimplemented by forking op_picker into a second module.

Rich TUI lives in the console; the CLI stays plain CLI

There are two distinct interactive surfaces, and the established principle keeps them apart: a rich ratatui TUI belongs only inside jackin console; the CLI never spins one up.

  • CLI --interactive. jackin workspace claude-token setup <ws> --interactive walks account → vault → item → field with plain CLI prompts (dialoguer Select/Input), offering [ + New item ] and [ + New field ] at the relevant steps. It does not open an alt-screen TUI. This keeps the CLI composable with a plain shell, scripts, and pipes, and means an operator at a terminal never has a second raw-mode UI fight their own.
  • Console wizard flow. The rich, multi-step flow above belongs in jackin console, opened from the Auth tab — already on screen when the operator wants to configure a token — using the console's own modal stack so the operator never leaves the console and keeps all of its context (workspace state, auth-tab focus, in-progress edits). Every screen reuses a shared component: source_picker, op_picker in Create mode, and the text_input box for the naming sub-stages. (Scope is the auth form itself, so no scope_picker/role_picker step appears in the generate flow.)

The shared op_picker Create mode, the plain-prompt CLI path, and the console-side generate-token action are all shipped. On a Claude oauth_token auth form, G opens a source step (Plain text / 1Password); for 1Password it opens op_picker in Create mode. Pressing G pushes the open auth form onto the modal parent stack, so the dialog can be re-mounted when the mint finishes. On commit the run_console loop suspends the terminal (leaves raw-mode + alt-screen, mirroring TerminalGuard), runs the claude setup-token OAuth mint + the 1Password item create / read-back validation via token_setup::mint_token_value (which mints and validates but does not write jackin config), then resumes — the one place in the console that hands the real terminal to a child process. On success the loop re-mounts the Edit-auth dialog with the minted credential staged and focus on Save (via the same apply_op_picker_to_auth_form / apply_plain_text_to_auth_form helpers the provide path uses); the credential is persisted only when the operator Saves, exactly like picking an existing value. Cancel leaves the config unwired (any op item already created is harmless, matching the provide path). The scope follows the auth-form target: workspace level, a per-role override (open that role's form), or global (Settings). The plain-text target stores the minted token as a literal at that scope; 1Password stores an op:// ref. The CLI path (token_setup::run_setup) keeps minting and persisting in one step because it has no form to return to. See Workspace Claude Token Setup.

Why reusability matters — the design principle behind this flow

The reason every screen above reuses an existing component is not code economy — it is operator trust. When the same meaning is always shown with the same shape, the operator learns the UI once and recognises it everywhere: a two-button dialog always means "pick one of two", a + row always means "add a new one", the bordered filter list always means "drill down", the input box always means "type a value here". A flow assembled from these known shapes is legible on first sight even though the operator has never seen this particular flow before. A flow that invents new shapes for the same meanings forces the operator to re-learn the UI at every screen and erodes that trust. Consistency in design is not polish; it is the contract that lets the operator predict what each screen does. This is why copy-pasting a component with slightly different look-and-feel is a bug, not a shortcut: it breaks the one-meaning-one-shape contract that makes the whole TUI predictable.

On this page