docs(specs): workspace manager TUI (PR 2 of 3)#164
Merged
Conversation
Design spec for PR 2 of the launcher-workspace-manager series. Adds an interactive workspace manager screen to the jackin launcher — list, create, edit, and delete workspaces without dropping to CLI. Reached via `m` from the existing Workspace picker; Esc returns to the launcher. Launch path stays keystroke-identical. Key design decisions from brainstorming (all settled, no open questions): - Entry model: separate Manager screen on `m` keypress; launch path unchanged. - Editor tab set: General · Mounts · Agents · Secrets-stub. Secrets placeholder locks in the final tab strip so PR 3 fills in the panel without a visual reshuffle. - Text-edit UX: modal push — centered overlay, one reusable TextInput widget. - Staging: explicit save via `s`. Pending changes drive dirty markers; Esc with pending opens Discard/Save/Cancel. - Create flow: mounts-first wizard — file browser for host source, dst auto-defaulted to the same absolute path as src (host-path mirror), workdir picked from mount dsts + ancestors (never free-text), name last with live uniqueness check. - Delete UX: single-line Y/N confirm modal. - Style: reuses jackin's existing digital_rain (src/tui/animation.rs), step_shimmer, spin_wait, and landing-page color tokens from docs/src/components/landing/styles.css. One new area-bounded rain widget extracted from animation.rs. Three new reusable widgets emerge (TextInput, FileBrowser, Confirm) that PR 3's Secrets tab will consume unchanged. All persisted writes flow through ConfigEditor (established in PR 1, merged in #162). Non-goals: per-(workspace × agent) env overrides (PR 3), global mount management (CLI only), agent lifecycle from manager (CLI only), CLI surface changes, CHANGELOG. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Amends the workspace manager TUI spec with a Third-party dependencies subsection that names the three ratatui ecosystem crates we'll adopt: - ratatui-textarea (v0.9.x) — single-line TextInput (ratatui-org owned) - ratatui-explorer (v0.3.x) — FileBrowser with folders-only wrapper - tui-widget-list (v0.15.x) — WorkdirPick list mechanics All three require the ratatui unstable-widget-ref feature flag. Rejected with rationale so reviewers don't re-litigate: tui-input (superseded by ratatui-textarea), tui-confirm-dialog / tui-overlay (Confirm modal is cheaper hand-rolled), rat-widget (too opinionated), throbber-widgets-tui / ratatui-cheese (we have spin_wait already), ratatui-toaster (banner is ~30 LOC with step_shimmer), tui-logger (jackin has no log or tracing framework today). Also updates Rollout section — "no new dependencies" was no longer accurate. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
7 tasks
donbeave
added a commit
that referenced
this pull request
Apr 25, 2026
Resolve identical-intent conflicts in two test fixtures: main and this branch independently fixed the same `echo -n` portability bug on macOS by switching to `printf`. Main used the more conventional quoted form `printf '%s'`; took main's version in both files. - src/operator_env.rs::op_cli_invokes_binary_and_returns_stdout - src/runtime/launch.rs::load_agent_injects_op_cli_resolved_value Also brings in main's docs(roadmap) updates and the workspace-manager TUI design spec doc that landed via #164. cargo build --all-targets clean. cargo nextest run -p jackin: 829/829. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave
added a commit
that referenced
this pull request
Apr 25, 2026
* docs(plans): workspace manager TUI implementation plan
Twenty-two-task TDD-shaped implementation plan for the workspace
manager TUI specced in #164. Ordered in seven phases:
1. Foundation — deps + module scaffolds + animation.rs refactor
2. Widgets — Confirm, TextInput, FileBrowser, WorkdirPick, PanelRain
3. State machine — ManagerState, EditorState, CreatePreludeState
4. Render — list view, editor (4 tabs), modal dispatcher
5. Input — modal-first key dispatch with per-stage routing
6. Integration — LaunchStage::Manager wire-in, m keybinding, full
editor + create key handling, ConfigEditor save/create/delete paths
7. Polish — style effects (boot reveal, save shimmer, toast expire),
integration test, final verification + PR
Each task is TDD-shaped: write failing test → run fails → implement →
run passes → commit. Complete code in every implementation step. No
placeholders.
Scope cut documented in self-review: tab-slider and panel-focus-glow
animations from the spec's Style section are omitted; they're cosmetic
and can land in a follow-up PR without rework.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): scaffold workspace manager modules + widget deps
Adds three ratatui ecosystem crates (ratatui-textarea 0.9,
ratatui-explorer 0.3, tui-widget-list 0.15) and enables ratatui's
unstable-widget-ref feature. Creates empty module structures at
src/launch/widgets/ and src/launch/manager/ to land typed setters,
widgets, and state transitions in subsequent commits.
No behavior change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(tui): extract render_rain_frame for reuse
Separates the per-frame rain rendering from digital_rain's event loop
so the upcoming PanelRain widget can render bounded-area rain without
duplicating the renderer. tick_rain and RainState become pub(crate)
for the same reason. Fullscreen digital_rain is rewritten to delegate
to render_rain_frame. No visible change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Confirm widget — Y/N modal
Hand-rolled Y/N confirmation dialog. Case-insensitive, Esc cancels.
~60 LOC + 5 tests. Used by delete-workspace and discard-changes flows.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): TextInput widget — single-line via ratatui-textarea
Wraps TextArea in single-line mode (intercepts Enter and Ctrl+M so
newlines are never inserted). Exposes a ModalOutcome<String> contract:
Enter commits, Esc cancels, everything else passes through to the
textarea for cursor / insert / backspace handling.
Cursor is placed at end of initial text on construction so editing
feels natural (backspace works immediately on prefilled values).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): FileBrowser widget — wraps ratatui-explorer
Folders-only filter, seeded from $HOME by default, adds 's' as
select-current-folder. Delegates all navigation (h/l/j/k/Enter/
Backspace/Home/End/PgUp/PgDn/Ctrl+h) to ratatui-explorer defaults.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): WorkdirPick widget — choice list via tui-widget-list
Derives the pick list from mount dsts + each ancestor up to /, with
labels (mount dst / parent / root). Deduplicates when multiple mounts
share ancestors. Enter commits selected path, Esc cancels.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): PanelRain widget — area-bounded phosphor rain
Wraps tui::animation's RainState engine for rendering into a bounded
Rect. Tick + render are separate so callers control frame rate.
Resizes state when the rect changes shape.
Adds RainState::new(cols, rows) constructor to animation.rs so the
widget can initialize state without duplicating the column/grid setup
that was previously inlined in digital_rain().
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): ManagerState + EditorState + CreatePreludeState types
Defines the top-level state machine per spec § 3: ManagerStage enum
(List / Editor / CreatePrelude / ConfirmDelete), EditorState with
dirty detection and change_count, CreatePreludeState with the
mounts-first wizard step enum, Modal enum with target enums, Toast
type, and constructors. Tests cover WorkspaceSummary derivation,
ManagerState::from_config, EditorState dirty detection, and
CreatePreludeState initial step.
ManagerStage and ManagerState carry a lifetime parameter propagated
from TextInputState<'a> (ratatui-textarea borrow). MountConfig lacks
Ord/Hash so change_count uses linear containment checks rather than
BTreeSet symmetric_difference.
Transitions and key handling are filled in by subsequent tasks.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): create-workspace wizard state transitions
Mounts-first flow: PickFirstMountSrc → PickFirstMountDst → PickWorkdir →
NameWorkspace. Each accept_* method advances the step. default_mount_dst
mirrors the host src path. default_name derives from the dst basename.
build_workspace assembles the final WorkspaceConfig.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): manager list view render
Renders ManagerStage::List: header banner, horizontal-split body
(workspace list + details pane), footer hint. Other stages rendered
by subsequent tasks (12: editor, 13: modal dispatcher).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): editor view render (all four tabs)
Renders Editor stage with General / Mounts / Agents / Secrets-stub
tabs, dirty markers on changed fields, save-count footer. Error
banner overlays the top of the tab body using --landing-danger
(#ff5e7a) for real errors.
Refactors top-level render to let stages declare whether they use
shared chrome (List, future ConfirmDelete) or their own full-screen
layout (Editor, future CreatePrelude).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): modal render dispatcher
Centers a modal Rect at 60x30 percent of the frame and dispatches to
the appropriate widget's render function based on the Modal variant.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): key dispatcher with modal precedence
Scaffolds handle_key with modal-first precedence: if a modal is open
anywhere in the state machine, events route to the modal handler
before per-stage handlers. Full editor + prelude wiring lands in
Tasks 16 / 17; this commit has stubs for those to keep the compiler
happy. List and ConfirmDelete stages are fully wired (navigation,
delete flow via ConfigEditor).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): LaunchStage::Manager + m keybinding
Adds a third launch stage and wires an m keypress from the Workspace
picker to transition into it. run_launch now takes AppConfig by value
+ &JackinPaths so the manager can open ConfigEditor. Footer hint in
the Workspace stage gains 'm manage'.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): editor key handling — tabs, save, discard, field edits
Implements Tab/Shift-Tab navigation between tabs, ↑↓ row selection,
Enter-to-edit (opens modal per field type), Space/* on Agents tab,
a/d on Mounts tab. s triggers save via ConfigEditor::edit_workspace
or create_workspace, with error banner on failure. Esc with pending
changes opens the Discard confirm modal; Esc with clean state returns
to the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): full create-workspace prelude flow
Chains the four modals (file browser → dst TextInput → workdir pick →
name TextInput) through CreatePreludeState. On completion, transitions
to Editor(mode=Create) with everything pre-populated. s in the editor
creates via ConfigEditor::create_workspace.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): manager style effects
Boot reveal on manager entry via tui::animation::digital_rain(400, None).
Save toast auto-expires after 3s. Shimmer: toast text flashes white during
the first 400ms post-show. JACKIN_NO_ANIMATIONS=1 disables the rain
transition.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* test(launch): end-to-end manager delete-workspace flow
Drives manager::handle_key with scripted key events (d, y). Asserts
the workspace is removed from on-disk config, the manager transitions
back to List, and the in-memory workspace list refreshes. Regression
guard against state-machine drift.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* style(launch): quiet clippy / fmt for workspace manager
Fix all clippy warnings introduced by the workspace-manager TUI (~1500
lines of new code): unnested or-patterns, collapsible-if, elidable
lifetimes, default-trait-access, items-after-statements, match-for-
equality, match-for-single-pattern, needless-pass-by-ref-mut,
doc-markdown (missing backticks), missing-const-for-fn, uninlined-
format-args, manual-Debug-non-exhaustive, large-enum-variant (allow),
too-many-lines (allow), unnecessary-trailing-comma, and the associated
fmt diff (11 files reformatted).
No logic changes; all tests pass (workspace_config_crud requires
--test-threads=1 due to pre-existing set_current_dir race).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): replace Workspace picker with manager as initial stage
The old Workspace picker stage is removed. LaunchStage::Manager becomes
the initial stage — jackin opens directly to the manager. Enter on a
workspace launches it (via the existing Agent picker); e opens the
editor; n creates; d deletes; q/Esc exits jackin. The m keybind is gone
— nothing to enter since we are already in the manager.
Esc from the Agent picker returns to the manager list (was: Workspace
picker, which no longer exists).
Also removes the mid-loop digital_rain(400, None) boot reveal that was
fighting with skippable_sleep's raw-mode toggling, which caused arrow
keys to print as raw escape sequences in the manager instead of being
captured by crossterm events.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): render active modals in manager
render_modal was defined but never called. Modals in Editor and
CreatePrelude stages transitioned correctly in state but had no
visible effect on screen — pressing n would silently put the user in
the create wizard with an invisible FileBrowser, making the create
and edit-field flows appear broken.
Also renders the ConfirmDelete variant's confirm modal directly
(ConfirmState on ConfirmDelete is a top-level field, not wrapped in
Modal::Confirm).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): modal footer hints + stage-aware manager footer
File browser and text input modals were rendering without any hint
about the keys to commit/cancel. Users opening the create workspace
flow saw the folder picker but couldn't progress — pressing Enter
only descended into folders (s is the select key for ratatui-explorer),
and there was no visual cue about the right key.
Adds a one-line phosphor-dim italic footer inside each modal:
- FileBrowser: ↑↓ navigate · Enter open · h/← up · s select · Esc cancel
- TextInput: Enter confirm · Esc cancel
Also makes the top-level manager footer hint stage-aware:
- List stage: existing navigation hint (unchanged)
- CreatePrelude: Create workspace · follow the prompts · Esc cancel
- ConfirmDelete: Y yes · N no · Esc cancel
- Editor: still delegates to render_editor's own footer
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): Esc in create wizard returns to list immediately
Previously pressing Esc inside the first create-wizard modal cleared
the modal but left the state machine stuck in ManagerStage::CreatePrelude
with no modal active — render drew a blank body, requiring a second Esc
to reach the non-modal prelude handler that transitions back to List.
Now the post-modal check distinguishes three outcomes: in-progress
(modal still open), complete (wizard finished with name), and
cancelled (modal cleared without a name). Cancelled transitions to
List in the same input pass.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): wire rename + render all agents on Agents tab
Fixes two spec gaps:
1. Agents tab rendered only currently-allowed agents, so the user
couldn't add agents via Space toggle — non-allowed agents were
invisible. Now iterates config.agents (the full set) and shows
[x] or [ ] per agent based on pending.allowed_agents membership.
Threads &AppConfig through manager::render → render_editor →
render_agents_tab. Also fixes set_default_agent_at_cursor to use
config.agents for cursor-to-agent resolution instead of the
allowed-only list.
2. Workspace rename was a TODO. Now:
- ConfigEditor gains rename_workspace(old, new) using toml_edit's
key-rename (preserves nested tables + array-of-tables). Rejects
empty new name, collision, and missing old name.
- General tab's name row is editable on Enter (in Edit mode) via
TextInput modal.
- apply_text_input_to_pending stashes the name on
EditorState::pending_name.
- save_editor calls rename_workspace before edit_workspace when
pending_name differs, then updates editor.mode so subsequent saves
target the new name.
- change_count + is_dirty + render dirty marker all track the rename.
Tests: three new unit tests on ConfigEditor::rename_workspace covering
happy path (nested tables preserved), collision rejection, and empty-
name rejection.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): FileBrowser s commits cwd, not highlighted entry
Pressing s in an empty or file-only folder previously committed the
highlighted entry, which in such a folder is '../' — so the user got
the parent directory, not the folder they were viewing. This was
especially bad for newly-created empty workspace source folders.
Now s commits the explorer's current working directory via
FileExplorer::cwd() (ratatui-explorer 0.3.x). User intent is preserved:
'I've navigated to this folder — select it.' Footer hint updated from
's select' to 's use this folder' to reflect the semantics.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): render active-row cursor in editor tabs
EditorState::active_field tracked the cursor but render functions
didn't display it — users couldn't tell which row Enter / Space / * /
a / d would target. Add a ▸ prefix and phosphor-green bold to the
selected row across all three tabs (General, Mounts, Agents).
Also clamp Down-arrow to the last valid row so the cursor can't run
off the end of the visible content, and thread &AppConfig through to
handle_editor_key's Down handler so it can size the Agents tab's
row count correctly.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): UX polish pass on manager create/edit flows
Nine polish fixes reported after live walkthrough:
1. TextInput/Confirm modals were 30%-of-screen tall, rendered as big
empty boxes around a single line. Now variant-aware: inputs/confirms
are 5-6 rows fixed; file browser and workdir pick stay taller for
their scrolling lists.
2. 'last used' row hidden in Create mode (no history exists).
3. 'default agent' row hidden in Create mode (no agents picked yet).
4. Footer hint is now row-contextual: 'Enter rename' on name row,
'Enter pick workdir' on workdir row, 'a add / d remove' on mounts,
'Space toggle / * set default' on agents, nothing on read-only
rows. Base hint says 's save workspace' (was 's save') for clarity.
5. File browser gets a prominent outer block titled '<cwd> · press
[S] to use this folder' — the select affordance was previously
buried in a dim footer line.
6. Mount rows collapse 'src → dst' to just 'path' when src == dst
(host-path-mirror default — redundant arrow gone).
7. Mounts tab '+ Add / − Remove selected' footer uses white-bold for
the action words to distinguish from the mount list.
8. Agents tab gets a top banner clarifying empty = 'all allowed'
semantics vs non-empty = custom allow-list.
9. Read-only rows (last used) no longer advertise Enter in the footer.
Also fix max_row_for_tab in input.rs: Create mode General tab only
has 2 rows (name read-only + workdir), not 4.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): Confirm modal shows Y/N as styled buttons
Previously the modal rendered '[Y]es · [N]o (default) · Esc cancel' as
inline text inside a tall 30%-of-screen box, which looked like a
multi-line textarea. Now:
- Modal is compact (6 rows)
- Yes/No render as inverted-video buttons, centered
- No (default) uses white-on-black to distinguish as default action
- Esc cancel moves to a dim italic footer hint at the bottom
- Prompt text stays bold-white at the top
Enter intentionally unbound — destructive confirms should not commit
on accidental Enter presses.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): remove nested borders in FileBrowser modal
ratatui-explorer's widget renders its own bordered block with the CWD
as title. The prior polish pass added an outer block with 'press [S]
to use this folder' — the result was double borders, ugly and
confusing.
Drop the outer block. Show the 'press [S]' affordance as a bold-white
centered line ABOVE the explorer (no border), and keep the dim
navigation hint as a line BELOW. The explorer's own cwd-titled block
stays — no nesting.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Confirm modal gains Tab focus + Enter commits focused
Adds standard confirmation-dialog UX: Tab / Shift+Tab / ←→ / h/l cycle
focus between Yes and No; Enter commits the focused button. Default
focus is No (destructive action protection — accidental Enter won't
commit Yes). Y/N direct shortcuts still work regardless of focus.
Visual: focused button gets white bg + black text + bold; unfocused
gets phosphor-green bg. Footer hint updated to mention Tab + Enter.
Modal grows from 6 to 7 rows for a second spacer between buttons
and hint (was visually cramped).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): shrink FileBrowser + WorkdirPick modal heights
Prior sizing was 60 rows for FileBrowser and 40 rows for WorkdirPick —
effectively fullscreen on a typical 40-50 row terminal. Tight 20 and
12 rows fit comfortably and still show enough entries without the
modal swallowing the whole screen.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): + Add mount is a selectable sentinel row
Mirrors the + New workspace sentinel in the manager list. The Mounts
tab now renders + Add mount as a real selectable row at the end of
the list, selected via ↑↓, activated via Enter. Visual treatment is
white bold (distinguishing it from the green mount rows).
- max_row_for_tab reports len() (mount count + sentinel index) for
Mounts so ↓ can reach the sentinel.
- remove_mount_at_cursor is a no-op on the sentinel (guard already existed).
- a (anywhere on the tab) still works as a quick-add shortcut.
- Contextual footer hint differentiates between 'on a mount row'
(d remove · a add) and 'on the sentinel' (Enter add · a add).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Agents tab shows [all] / [custom] status badge
Replaces the implicit 'empty list = all allowed' with an explicit
status line at the top of the Agents tab:
Allowed agents: [ all ] (when allowed_agents is empty)
Allowed agents: [ custom ] (3 of 5 allowed) (when non-empty)
The badge is an inverted-video token (phosphor-green bg for 'all',
white bg for 'custom') making the current mode immediately visible.
The agent list below stays as a checklist — toggling updates the
status badge live.
Cursor semantics also shift: cursor is now 0-based into config.agents
(no more header-offset-by-one). toggle_agent_allowed_at_cursor and
set_default_agent_at_cursor are updated accordingly. max_row_for_tab's
Agents arm drops to len()-1.
set_default_agent_at_cursor now also auto-allows the agent being set
as default (was previously a no-op if the agent wasn't already in
allowed_agents).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Mounts tab shows folder / git · <branch>
Adds a mount_info helper that inspects the host-side src path on
render: checks for .git as dir or submodule-gitfile, reads HEAD, and
reports the current branch (or detached short-sha). Renders next to
each mount row as dim italic metadata:
/Users/…/repo (rw) · git · main
/Users/…/scratch (rw) · folder
/Users/…/gone (rw) · missing
Six unit tests cover: missing path, plain folder, normal repo with
branch, detached HEAD, submodule .git file, label formatting.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Save/Discard/Cancel modal + richer details pane
Two UX upgrades:
1. Exit-with-changes now offers three explicit choices instead of
binary 'Discard Y/N'. New SaveDiscardCancel modal with three
buttons (Save / Discard / Cancel), Tab cycles focus, Enter commits
the focused option. S/D/C/Esc shortcuts work regardless of focus.
Default focus is Cancel (safest). Save intent triggers ConfigEditor
save → list; Discard just drops pending; Cancel keeps the editor.
2. Manager list's details pane now shows the full mount list (with
folder / git · <branch> labels, same as the Mounts tab) and the
allowed-agents list (or 'any agent' when unrestricted). Title drops
the duplicate workspace name since the list selection already shows
it.
5 new unit tests on SaveDiscardState covering focus cycling and key
shortcuts.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): restrict FileBrowser to \$HOME, rename main title
Four UX fixes:
1. Main manager screen title 'manage workspaces' → 'workspaces'
(the screen does more than manage — launch, create, edit, delete).
2. FileBrowser modal goes fullscreen (100% x 100%) so the main chrome
doesn't peek through and confuse the visual.
3. FileBrowser now:
- Starts at \$HOME (already did)
- Excludes Library, Applications, Movies, Music, OrbStack, Pictures
from the listing via filter_map
- Clamps cwd back to \$HOME if the user escapes above it via
set_cwd() (ratatui-explorer 0.3.x has this method)
- Rejects \$HOME itself as a workspace source
- Rejects ~/.jackin/* (jackin's reserved data area)
4. Rejected selections show an inline red error banner
(#ff5e7a) above the explorer. Cleared on next keypress.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): display paths as ~/… via shorten_home
Paths starting with $HOME now render as '~/...' in the TUI:
General tab workdir, Mounts tab rows, details pane mounts/workdir,
WorkdirPick choices. Consistent with jackin's existing shorten_home
helper (already used elsewhere in the launcher).
Paths stored on disk are unchanged — this is display-only.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): mount table formatting + FileBrowser resize/colors
Two fixes:
1. Mount lists in the details pane and Mounts tab render as an
aligned 3-column table (path, mode, type) instead of a free-form
line where the '(rw)' tag and type metadata floated at variable
positions. shorten_home applied to paths consistently via the
shared format_mount_rows helper, which is called from both
render_details_pane and render_mounts_tab.
2. FileBrowser modal goes from fullscreen (100%) to 70%x70%, letting
the surrounding chrome show again so the dialog reads like a
dialog, not a whole screen. Theme configured to use jackin's
phosphor palette (green text, bright-phosphor highlight, shortened
CWD title via shorten_home in a dynamic with_title_top closure).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): drop Agents 'default' column + cap workspace list height
1. Agents tab header 'allowed? · default · agent' → 'allowed? · agent'.
The star marker next to the agent name already indicates default;
the dedicated column was empty for every non-default row.
2. Manager list body now caps at content height (workspace count + 2
border rows + 1 sentinel row) instead of filling the whole frame.
5-6 workspaces no longer render in a box that looks two-thirds
empty.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* Revert "fix(launch): cap workspace list height to content"
The height cap made the space below the boxes visibly empty, which
reads worse than the previous full-height boxes. User feedback:
'before it was better when it was using the whole vertical space.'
Keeps the Agents tab header change from the same original commit
(3fdab9f3) — only the list-body sizing is reverted.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): hide FileBrowser .. entry at $HOME root
Previously the '../' entry was always shown in the file browser.
When the user was at $HOME, selecting it would escape the sandbox
(and was then clamped back by set_cwd) — confusing and cluttered.
Now the filter hides '..' when its target path is outside the root
subtree. At $HOME the entry disappears; at any subfolder of $HOME
it still appears so the user can navigate back up.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): hide empty right pane, split details, clickable git links
Three UX improvements:
1. When the cursor is on '+ New workspace' in the manager list,
the right details pane is hidden entirely — the list takes full
width. No more empty bordered box for the sentinel row.
2. Details pane split into three stacked sub-panels: General (workdir
+ last used), Mounts (tabular with header row), Agents (list or
'any agent'). Each has its own bordered mini-block with phosphor-
dark border and white-bold title. The outer 'Details' block is gone.
3. Git branch URL resolution wired up: inspect() now parses
<git_dir>/config to find the origin remote and derives a web URL
(GitHub, GitLab, generic HTTPS/SSH). MountKind::Git gains a
web_url: Option<String> field; MountKind::labeled_hyperlink() wraps
the branch name in OSC 8 escape sequences for supported terminals
(iTerm2, kitty, WezTerm, Alacritty, modern Terminal.app).
OSC 8 fallback: ratatui's Paragraph widget strips raw ESC bytes, so
the render path continues to call label() (plain text). The
hyperlink infrastructure (labeled_hyperlink, osc8_link, web_url) is
retained for a future raw-terminal-write path. Both are annotated
#[allow(dead_code)] with an explanatory TODO.
5 new unit tests on remote-URL parsing (GitHub SSH, GitHub HTTPS,
ssh:// protocol, GitLab SSH, config-file parse). All 566 tests pass.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): polish FileBrowser, WorkdirPick, and mount-dst prompt
- FileBrowser entries now render white instead of phosphor-green so the
bright-green highlight is the unambiguous focus indicator.
- TextInput prompts for mount destination say "destination (default:
same as host path)" instead of the internal "Mount dst" phrasing.
- WorkdirPick lines are laid out as a table: the path column is padded
to the widest choice so the dim+italic label column (`(mount dst)`,
`(parent)`, `(root)`, `(home)`) lines up cleanly.
- WorkdirPick filters `/` and the literal parent of `$HOME` (e.g.
`/Users` on macOS, `/home` on Linux) from the choice list — those
paths are never useful workdir targets.
- When a path is exactly `$HOME`, label it `(home)` instead of
`(parent)` so the workspace operator sees a recognisable name.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): FileBrowser s commits highlighted folder
Previously `s` always committed the explorer's cwd, which meant the
operator had to press Enter to navigate into the target folder before
committing — even though the folder was already highlighted and the
target of a single Enter press.
Reading `FileExplorer::current()` lets us commit the highlighted entry
directly when it is a real child directory. The synthetic `../`
parent-link row and the empty-listing case both fall back to the cwd,
preserving the previous behaviour for those edge cases.
The existing $HOME and `~/.jackin/*` rejection rules apply to whichever
path is chosen as the commit target.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): keep General-tab labels white; note agent-hyperlink TODO
- render_editor_row and render_editor_readonly_row no longer shift the
label column to phosphor-green when the row is focused. Labels stay
white (bold when focused); values keep their phosphor colouring for
editable rows and dim phosphor for read-only rows.
- Read-only rows used to render everything in phosphor-dim, which made
the editor view look washed-out. They now match the editable-row
label treatment (white) with a dim value + italic "(read-only)"
suffix, giving the operator a cleaner signal-to-noise ratio.
- Added a TODO in render_agents_subpanel mirroring the existing
labeled_hyperlink() note in render_mounts_subpanel: ratatui's
Paragraph strips OSC 8 ESC sequences, so agent-name → GitHub links
stay plain-text until a raw-write path exists.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): gate editor save on mount-collapse plan
The editor used to write configs straight to disk via
`ConfigEditor::edit_workspace` (or `create_workspace`), which meant the
operator could save a workspace with overlapping mounts like
`~/Projects` and `~/Projects/test`. The CLI rejects this unless you
confirm or pass `--prune`; the TUI now does the same.
Flow:
- On `s`, run `workspace::planner::plan_edit` (Edit) or `plan_create`
(Create) against the pending mount set.
- `CollapseError::{ReadonlyMismatch, ChildUnderExistingParent}` ->
error banner, no write.
- Pre-existing collapses only (no edit-driven) -> error banner
referencing `jackin workspace prune <name>`. The operator can't fix
these from the editor alone and the CLI prune command already exists
for this case.
- Edit-driven collapses -> open a `Modal::Confirm` with a
`ConfirmTarget::SaveCollapse` target, listing each child/parent pair
in the same wording as the CLI. On Yes, the save re-enters with
`EditorState::collapse_approved = true` and commits the collapsed
mount set via `plan.effective_removals` / `plan.final_mounts`. On No
/ Esc, pending mounts are kept intact so the operator can edit by
hand.
Pattern: a boolean flag on `EditorState` + a new `ExitIntent::RetrySave`
variant so the confirm-yes path reuses the existing modal-exit routing
but stays in the editor on success (rather than bouncing to the
workspace list, which is what `ExitIntent::Save` does). The plan
itself is not stashed; it is cheap to recompute on re-entry.
The `Confirm` widget now grows its prompt region to match the number
of lines in `state.prompt`, and `render_modal` sizes the outer rect
via `confirm::required_height` so multi-line collapse summaries render
without clipping.
Tests (5 new):
- `save_editor_opens_confirm_on_edit_driven_collapse`
- `confirming_collapse_writes_collapsed_set`
- `cancelling_collapse_keeps_pending_mounts_intact`
- `readonly_mismatch_produces_error_banner_no_write`
- `pre_existing_collapse_produces_prune_error_banner`
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): structured footer with per-item styling
Introduce a `FooterItem` enum (Key / Text / Dyn / Sep / GroupSep) and a
shared `render_footer` that emits spans with a consistent palette:
- Key glyphs (↑↓, Enter, e/n/d/q, Tab, Esc, S, Y/N, *, Space) render in
WHITE + BOLD so they pop out of the legend.
- Action labels ("launch", "edit", "new", …) render in PHOSPHOR_GREEN.
- Inline dots (·) render in PHOSPHOR_DARK as a faint separator.
- A GroupSep (three spaces, no style) introduces a wider visual gap
between logical groups — navigation, per-row actions, and exit.
Migrate every footer call site to this scheme:
- `manager/render.rs` List / CreatePrelude / ConfirmDelete / Editor
footers build `Vec<FooterItem>` explicitly so the grouping is
deliberate per stage.
- Agent-screen footer in `launch/render.rs` uses the same inline spans.
- Modal-local hints inherit the scheme (file_browser navigation + "[S]
to use this folder" affordance, text_input "Enter confirm · Esc
cancel", confirm "Tab cycle · Enter confirm · Y yes · N no", and
save_discard).
Add unit tests covering the span-style mapping per variant plus
smoke tests for the List and ConfirmDelete stage footers.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): keep right pane visible on '+ New workspace' sentinel
Batch 7 expanded the list to full width when the cursor landed on the
sentinel row. The operator wants the 45/55 split preserved — the layout
should not shift as the cursor moves — with the right pane rendered as
an empty bordered block (same PHOSPHOR_DARK border as the General /
Mounts / Agents sub-panels) when there is no workspace to describe.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align mount-table header with data columns
The mount-table header was a hardcoded string (" path<23 spaces>mode<3
spaces>type") while data rows computed their path column width
dynamically from the widest row. When paths were shorter than 23 chars
the header appeared drifted relative to the data; when they were longer
the header's "mode" column collided with the data's mode column at a
different offset.
Share the column-width computation between the header and data rows:
- Extract `mount_path_width` which returns max(row_path, "path".len(),
10) so the header and data always use the same column boundary.
- Add `render_mount_header(path_w)` that uses the same format string as
the data rows, then have both the read-only details subpanel and the
editor Mounts tab consume it.
- Pin the `mode` column to a shared `MOUNT_MODE_COL_WIDTH = 4` constant
(covering "mode" as well as "rw"/"ro" + trailing space) so it no
longer over-pads inconsistently.
Add unit tests that build mount rows with mixed path lengths and assert
the header's "mode" column starts at the same character index as each
data row's "mode" column.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): describe workspace concept on '+ New workspace' pane
Replace the empty bordered block shown to the right of the manager list
when the sentinel row is focused with a two-panel description pulled
from the "What is a workspace?" / "Why save a workspace?" sections of
the workspaces guide. Keeps the right-hand real estate useful for
first-time operators and matches the General/Mounts/Agents sub-panel
chrome for visual consistency.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): restore 'Current directory' row in workspace manager
Before the TUI redesign the launcher's first row was a synthetic
"Current directory" choice that let operators launch an agent against
cwd without saving a workspace. The manager's rewrite dropped it; this
reinstates it as row 0 of the list with the right-pane summary, the
cwd-aware preselect, and the launch wiring that matches the old
behaviour.
Row layout (enforced by ManagerState::from_config, render_list_body,
and handle_list_key):
row 0 → synthetic "Current directory"
rows 1..=N → saved workspaces
row N+1 → "+ New workspace" sentinel
Edit (`e`) and Delete (`d`) are rejected on row 0 with a toast. Enter
on row 0 emits a new InputOutcome::LaunchCurrentDir; the run-loop
routes it through the same agent-picker transition as LaunchNamed,
reusing LaunchState::workspaces[0] (the CurrentDir choice built by
LaunchState::new). Preselect reuses find_saved_workspace_for_cwd so
TUI and CLI agree on "which workspace am I in?".
The right pane branches on row 0 → render_current_dir_details_pane
(dedicated renderer; no last-used row, no edit affordance, "any
agent"). The sentinel description pane lands in the same commit's
sibling already; saved-workspace rows continue to use the shared
render_details_pane with `workspaces[selected - 1]`.
Tests added:
- manager_preselects_saved_workspace_matching_cwd
- manager_preselects_current_directory_when_no_saved_matches
- manager_current_directory_is_first_row
- current_directory_row_rejects_edit_and_delete
- enter_on_current_directory_returns_launch_current_dir
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): polish mount-header gap, modal titles, and FileBrowser size
- Mount table header: add two-space gutter between `mode` and `type`
so the header no longer reads "modetype". Data rows now emit the
matching two-space gap so the `type` column aligns in both the
read-only Mounts subpanel and the editor Mounts tab.
- Text-input + Workdir-pick modal block titles render WHITE + BOLD to
match the General/Mounts/Agents block titles on the main screen.
Confirm + SaveDiscard already use WHITE+BOLD — left untouched.
- WorkdirPick path values render WHITE (the `(mount dst)`/`(parent)`/
`(home)`/`(root)` label suffix stays PHOSPHOR_DIM italic).
- FileBrowser modal height drops from 70 absolute rows to 22 so it
no longer eats the whole screen. Width stays at 70%.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): classify git mounts by host and relabel GitHub remotes
- Introduce `GitHost { Github, Other }` on `MountKind::Git` so the
render path can tell which remotes have an "open in browser"
affordance. `inspect` populates this from `parse_remote_origin_url`:
SSH `git@github.com:`, HTTPS `https://github.com/…`, and
`ssh://git@github.com/…` all resolve to `Github`; anything else
(self-hosted, GitLab, no remote, unparseable URL) falls through to
`Other`.
- `remote_to_web` now returns `Some(url)` only for GitHub hosts and
`None` for everything else — it no longer synthesises `gitlab.com`
URLs. Non-GitHub remotes keep `web_url: None` on the `MountKind`.
- `MountKind::label()` renders `github · {b}` / `github · detached {sha}`
/ `github` for GitHub hosts and keeps the generic `git · …` prefix
for `Other`. `MountKind::Folder` / `Missing` unchanged.
- `remote_to_web_gitlab` test re-purposed to assert GitLab (and other
non-GitHub hosts) now return `None`. New tests for the GitHost split
via `inspect` and for the `remote_points_at_github` predicate covering
all three URL forms + a GitHub-lookalike subdomain rejection.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): 'o' key opens highlighted GitHub mount in the browser
- Add the `open` crate (5.x) so the editor can launch the system
browser without blocking the TUI (`open::that_detached`).
- Wire `o` into the editor's Mounts tab: when the cursor is on a
mount row whose source resolves to a GitHub-hosted repo with a
web URL, pressing `o` opens that URL in the operator's default
browser. Non-GitHub / folder / missing mounts emit an "no GitHub
URL for this mount" toast so the hint is discoverable; the sentinel
"+ Add mount" row is a silent no-op.
- `contextual_row_items` now composes an `o open in GitHub` item
onto the existing `d remove · a add` pair when the current row is
a GitHub mount. List-view mounts pane is unchanged — the `o` key
only binds in the editor.
- Tests: `github_mount_row_includes_open_in_github_hint` and
`non_github_mount_row_omits_open_in_github_hint` pin the footer
composition. No unit test for the browser side-effect itself.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): flag git repos in FileBrowser and offer mount-or-dive prompt
Part A — directory listing:
- `annotate_file` (new filter_map body) stats each directory for a
`.git` child and appends U+25A3 (▣) to the display name when present.
Works for plain clones (`.git` is a directory) and submodules (`.git`
is a file containing `gitdir: …`). Single stat per entry — no
recursive walk.
Part B — Enter on a git-repo row:
- New `GitPromptFocus { MountHere, EnterIn, Cancel }` + two fields on
`FileBrowserState` (`pending_git_prompt`, `pending_git_focus`) drive
an in-widget confirm overlay. Enter on a git-repo row opens the
prompt; Tab/←→/h/l cycle focus; Enter commits the focused option;
M/E/C are direct shortcuts; Esc dismisses the prompt without
cancelling the browser.
- MountHere commits the repo path through the same sandbox rules as
`s` (rejects root / `~/.jackin/*`). EnterIn navigates into the repo
via `explorer.set_cwd` (avoids re-posting Enter, which would re-open
the prompt). Cancel just clears state. Non-git folders keep their
usual Enter-navigates-in behavior.
- Overlay renders as a centred 3-button bar inside the explorer area
so the listing stays visible as context. Phosphor palette + focus
styling mirrors `confirm.rs`/`save_discard.rs`; button ring copied
locally rather than cross-importing between widgets.
- Footer legend swaps to `Tab cycle · Enter confirm · Esc cancel`
while the prompt is active.
Tests cover: marker on `.git`-dir and submodule-`.git`-file cases, no
marker on plain folder, Enter opens prompt on repo row, MountHere
commits the path, EnterIn navigates in and clears prompt, Cancel
clears without cwd change, Esc dismisses prompt without cancelling
browser, plain-folder Enter navigates as before, and the `M` shortcut
commits regardless of current focus.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): add mount-destination choice modal widget
Introduces the `mount_dst_choice` widget and wires a new
`Modal::MountDstChoice` variant into the manager's modal enum plus
render dispatcher. No input behaviour changes yet — follow-up commits
swap the Editor and Prelude FileBrowser→TextInput chains to route
through this modal.
The widget is the 3-button focus-ring pattern pioneered by
`save_discard`: default focus on `OK`, Tab/BackTab cycling, and single-
letter shortcuts (`o`/`e`/`c`). Default on `OK` because the common case
is `dst = src`, so an accidental Enter commits that without surprise.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): route editor add-mount through destination choice modal
The FileBrowser→TextInput chain in the Editor's Mounts tab assumed
every operator wanted to edit the destination path. In practice, 95%
of mounts commit with dst = src. Swapping in the new MountDstChoice
modal makes the common path a single Enter press and keeps the old
behaviour one keystroke away via `Edit destination`.
`apply_file_browser_to_editor` now opens MountDstChoice instead of
pushing a provisional mount plus TextInput. The actual push happens
in the MountDstChoice commit handler:
- OK: push MountConfig { src, dst = src, rw }, close modal.
- Edit destination: push the provisional mount (as today) and open
Modal::TextInput{MountDst} pre-filled with src. The existing
TextInputTarget::MountDst handler overwrites the provisional dst.
- Cancel / Esc: close the modal, leave pending.mounts untouched.
Behavioral tests pin all three paths and guarantee no mount is
pushed until the operator commits in the choice modal.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): route create-prelude add-mount through destination choice
Mirrors the editor-side change: the Create wizard's FileBrowser step no
longer assumes the operator wants to edit the destination. Instead, the
prelude now opens MountDstChoice after FileBrowser commits, offering
the fast `OK` path that skips TextInput entirely.
Both paths (OK and TextInputDst commit) share a new helper
`prelude_advance_to_workdir_pick` so the downstream WorkdirPick stage
receives the same staged mount regardless of whether the operator
edited the destination. This keeps the chain FileBrowser → (choice) →
WorkdirPick → TextInput(Name) intact for the `OK` shortcut.
Cancel on MountDstChoice matches today's Esc-during-TextInput
behaviour: close the modal, leave the prelude state alone so the
outer dispatcher treats it as a wizard-cancellation and returns to
the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): extract mount-dst-choice dispatch to keep clippy happy
The inline `Modal::MountDstChoice` arm inside `handle_editor_modal`
pushed the function above clippy's 100-line ceiling. Extract the
outcome dispatch into `dispatch_editor_mount_dst_choice` and tidy the
helper's doc comment so `TextInput` doesn't trip the missing-backticks
lint. No behavioural change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): strip trailing slash in FileBrowser name filter
ratatui-explorer appends `/` to directory names at runtime, so the
filter in `annotate_file` was comparing `"Library"` against `"Library/"`
and silently letting every excluded entry render. Same bug let `..`
through on the sandbox-escape check. Normalize with
`trim_end_matches('/')` before matching, and harden the `s`/Enter
paths + default key dispatch to guard against empty listings (the
fixed filter can now produce an empty `files()` which made
`current()` and nav-key dispatch panic inside ratatui-explorer).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* polish(launch): simplify Destination modal title
Rename the mount-destination TextInput label from
`destination (default: same as host path)` to plain `Destination`.
The parenthetical hint is redundant after batch 12: the TextInput
only opens when the operator explicitly picks "Edit destination"
on the MountDstChoice modal, so they're already in deliberate-edit
mode with the src pre-filled as the default.
Also capitalizes the title to match the other modal block titles
(Confirm, Unsaved changes, Mount destination, Git repo detected,
Workdir pick, Rename workspace, Name this workspace). No other
titles needed changes — the audit was clean.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): bind Left/Right to prev/next tab in Editor
Extend `handle_editor_key` so Right matches Tab (forward cycle) and
Left matches BackTab (reverse cycle). Wrap-around behavior mirrors
the existing Tab contract: General → Mounts → Agents → Secrets →
General, and symmetrically for reverse.
Modal-open precedence is already guarded by the early-return in
`handle_key` — Left/Right continue to feed into modal handlers
(Confirm, SaveDiscard, MountDstChoice) when a modal is active.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): wire list-view `o` to open workspace GitHub mounts
Extend the `o` key beyond the Editor's Mounts tab to the workspace
list view. On a saved workspace row:
0 GitHub mounts → toast "no GitHub URLs for this workspace"
1 GitHub mount → open::that_detached immediately
≥2 GitHub mounts → open a new GithubPicker modal; Enter commits
the highlighted URL to open::that_detached.
Row 0 (Current directory) and the `+ New workspace` sentinel toast
`no workspace selected` for discoverability. The list footer now
surfaces `o open in GitHub` only on rows whose workspace resolves
to ≥1 GitHub-hosted mount.
Adds:
- new `widgets/github_picker.rs` widget (title-styling and tab-list
pattern mirror WorkdirPick so the modal feels native);
- `Modal::GithubPicker { state }` variant, with arms closed in the
render-size switch, `handle_editor_modal` (defensive cancel), and
a new `handle_list_modal` dispatcher;
- `list_modal: Option<Modal<'a>>` slot on ManagerState — list-view
modals weren't previously anchored anywhere; Editor/CreatePrelude
keep their per-stage modal slots unchanged;
- `resolve_github_mounts_for_workspace` helper, shared by the input
handler and the render-side footer-hint guard.
Piggybacks a one-line clippy fix in file_browser.rs
(`iter().any(|x| *x == bare)` → `EXCLUDED.contains(&bare)`) that
surfaced after the trailing-slash filter landed.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): drop cwd suffix from Current directory row label
The list row for the synthetic "Current directory" choice used to read
`Current directory (~/Projects/foo)`. The right-pane details already
show the cwd on the `workdir` line, so the parenthetical suffix is
duplicate visual load. Render just `Current directory`.
Row 0 keeps its WHITE colour so the synthetic choice still visually
separates from the phosphor-green saved workspaces below it.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align mount-table type column with header
MOUNT_MODE_COL_WIDTH was 2, matching the literal width of rw/ro but
leaving a 2-char gap before the data row's kind column versus the
header's 4-char "mode" label. Header and data rows shared the same
"{mode:<mw} type" format string but MOUNT_MODE_COL_WIDTH no longer
matched the header label length, so `type` and its data (e.g. "folder")
rendered at different offsets.
Pin MOUNT_MODE_COL_WIDTH to 4 so rw/ro pad to the header's "mode"
width. Both the header and data emit the same two-space gutter before
the `type` column, so the kind label lines up with the header offset.
Extend the existing gap-between-mode-and-type test to additionally
assert that `header.find("type") == data.find("folder")` — the
type-column offset must match for a row with a plain folder mount.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): drop dead OSC 8 hyperlink scaffolding
The `o`-opens-in-system-browser path (via open::that_detached)
supplanted the aspirational OSC 8 hyperlinks-in-terminal route. The
OSC 8 helpers were already #[allow(dead_code)] — remove them:
- `osc8_link()` — wrapped text in OSC 8 ESC sequences;
- `MountKind::labeled_hyperlink()` — built a GitHub-linked label
from a branch/sha + url;
- their associated NOTE blocks on render.rs (mounts subpanel) and
the agents-hyperlink TODO next to render_agents_subpanel.
The `web_url: Option<String>` field stays — the `o` key consumes
it to open the branch URL. Likewise `remote_to_web`,
`parse_remote_origin_url`, and the `GitHost::Github` classification
are all still in the live path.
Clippy baseline (4) unchanged; no tests touched.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): mouse-draggable list/details divider
Add a draggable seam between the workspace list pane and the details
pane in the manager TUI. Click-and-drag on the seam column (within ±1)
resizes the split; the percentage is clamped to [20, 80] so neither
pane can be starved.
Mechanically:
- ManagerState gains `list_split_pct: u16` (default 45) and
`drag_state: Option<DragState>`. `clamp_split` + split-range consts
live alongside. `render_list_body` reads `list_split_pct` instead
of the hard-coded 45/55.
- `src/launch/mod.rs` enables `EnableMouseCapture` after entering the
alternate screen and `DisableMouseCapture` in the terminal guard's
Drop. Side-effect: the terminal's native click-drag text selection
stops working while the TUI is running — hold Shift (Terminal.app,
iTerm2) or Option (iTerm2) to bypass. Documented inline.
- The run-loop now matches on `Event::{Key, Mouse, _}` (was a bare
`if let Event::Key`). Mouse events in the Manager stage dispatch
to a new `manager::input::handle_mouse` with the current terminal
size as a `ratatui::layout::Rect`.
- `handle_mouse` hit-tests the seam, captures a `DragState` anchor
on `Down(Left)`, updates `list_split_pct` on `Drag(Left)`, and
clears the anchor on `Up(Left)`. It also gates on List stage, no
open list-modal, and `term_size.width >= 40`.
Unit tests (8 new, pure state manipulation — no ratatui loop):
- mouse_down_on_seam_starts_drag
- mouse_drag_updates_split_pct
- mouse_drag_clamps_to_min_and_max
- mouse_up_ends_drag
- mouse_down_far_from_seam_does_not_start_drag
- drag_ignored_when_list_modal_open
- drag_ignored_on_non_list_stage
- drag_ignored_when_terminal_too_narrow
Clippy baseline (4) unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): rename current-dir pane first block to "General"
The synthetic "Current directory" row has a right-pane first block titled
" Current directory ", but the left-list row label already conveys that
context. Rename to " General " to match the saved-workspace details pane
(General / Mounts / Agents) so both panes use the same three sub-panel
titles.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): remove phantom empty row in current-dir Mounts block
`render_current_dir_details_pane` hard-coded the Mounts block at
`Constraint::Length(5)`, which over-allocated by one row for the
single-mount current-directory case and left a visible empty line
inside the block border.
Extract the height formula from `render_details_pane` into a shared
`mount_block_height` helper (2 borders + 1 header + max(1, N) data rows,
clamped to 12) and use it from both pane renderers so the two paths
produce identically-tight Mounts blocks.
Covered by four regression tests pinning the formula for the empty,
single, multi, and many-mount cases.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align General sub-panel content with Mounts/Agents
The General sub-panel (on both the saved-workspace pane and the
current-directory pane) rendered its `workdir`/`last` rows flush against
the block's left border, while the Mounts and Agents sub-panels already
used a two-space indent. The mismatch gave the right pane a jagged left
edge across the three stacked blocks.
Add the same two-space prefix to the General rows on both panes. The
convention is pinned by a new `SUBPANEL_CONTENT_INDENT` constant and
two visual regression tests that render each sub-panel to a
`TestBackend` buffer and assert the first visible character of row 0
sits at that indent relative to the block's left border. Covers the
"any agent" fallback and the starred-default-agent row explicitly.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): follow worktree commondir for GitHost detection
Git worktrees are checkouts whose `.git` is a file pointing at
`<main>/.git/worktrees/<name>`. That per-worktree gitdir owns HEAD but
has no `config` of its own — the shared config (including the remote
URL) lives at the target of a `commondir` pointer. The previous
`resolve_git_dir` stopped at the worktree-specific gitdir, so
`resolve_host_and_url` read nothing, `GitHost` defaulted to `Other`,
and the label rendered as `git · branch` instead of `github · branch`
for every worktree of a GitHub-hosted repo.
Split resolution into `resolve_gitdirs`, which returns a pair:
- `work_dir` — owns HEAD (worktree-specific for worktrees, identical
to `config_dir` for plain clones and submodules).
- `config_dir` — owns the remote URL (follows `commondir` when present,
handling both relative and absolute pointer forms).
`inspect` now parses HEAD from `work_dir` and the remote URL from
`config_dir`, so the host is re-classified correctly for worktrees
without perturbing the submodule path.
Covered by three new tests:
- `worktree_gitfile_resolves_to_commondir` (relative commondir)
- `worktree_commondir_with_absolute_path`
- `submodule_gitfile_still_resolves_host_end_to_end` (regression guard
for submodules — HEAD + config co-located, no commondir)
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): drop FileBrowser affordance banner
The "press [S] to use this folder" banner above the explorer is redundant
with the `S select` footer hint below it, and inconsistent with other
modal styling (no other modal has a top banner). Drop it and its layout
constraint so the explorer shifts up by one row.
The `rejected_reason` banner (shown when the operator picks $HOME itself
or a `.jackin/` path) stays — it is functional error feedback, not an
affordance hint.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): prefix git marker, "repo" -> "repository" in UI
Three small FileBrowser/git-prompt polish items:
- Prepend the git-repo marker instead of appending. Changed the glyph
from U+25A3 (white square with black small square) to U+2387
(alternative key symbol — reads as a branch) and moved it to the head
of the directory name so the eye lands on it first. The trailing
marker was easy to miss; a file listing now shows
"⎇ scentbird-root/" rather than "scentbird-root/ ▣".
Noted as a one-liner in the code: per-entry colouring would need
dropping ratatui-explorer — out of scope here.
- Rename user-facing "repo" to "repository". The button label becomes
"Mount this repository" and the prompt title becomes
"Git repository detected". Identifiers (repo_dir, GIT_REPO_MARKER,
test fixture names) are left alone — this is a UI-string change only.
- Rename the middle git-prompt button from "Enter to pick subdirectory"
to "Pick a subdirectory" — imperative voice parallel to the first
button, no more "key + verb" mix. Footer hints under the prompt
still read "E enter" for the shortcut, which remains correct.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): uppercase single-letter hotkeys in footer hints
Operator prefers the `M mount · E enter · C/Esc cancel` style
consistently across the TUI. Previously the list-view footer was
lowercase (`e edit · n new · d delete · o open in GitHub · q quit`)
while the git-prompt hint was uppercase. Normalise every footer site
on uppercase single-letter keys; multi-character glyphs (Enter, Tab,
Esc, ↑↓, etc.) and non-alpha keys (`*`) pass through unchanged.
Updates:
- `src/launch/manager/render.rs` list footer: E/N/D/O/Q
- `src/launch/manager/render.rs` editor save footer: S
- `src/launch/manager/render.rs` contextual_row_items: D/A/O on
Mounts rows, A on the "+ Add mount" sentinel
- `src/launch/widgets/file_browser.rs` nav hint: S select,
H/← up
- Existing footer test assertions updated to match new casing
- New `footer_hotkeys_are_uppercase` test scans contextual hints
(Mounts row + sentinel, Agents) and verifies every single-char
alphabetic `Key` item is uppercase
Key handlers extended to accept both cases where a footer now shows
uppercase. Most handlers already matched `'e' | 'E'` from batch 11;
the remaining lowercase-only sites (list Q/E/N/D/O, list K/J nav,
editor S/K/J, editor-Mounts A/D/O, file_browser S/H/L nav, picker
J/K) now take `'x' | 'X'`. Behavioural change is nil — Caps Lock
and Shift-held hotkeys now work where they already did at the
footer-advertised case.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): click-to-select in workspace list
Extend `handle_mouse` with row-click selection on the workspace-list
pane. Left-button Down inside the list content area maps to the row
index and updates `ms.selected`; clicks on the seam column still
start a drag (regression guard for batch 14).
Hit-test rules:
- Seam always wins — a click within ±1 column of the current seam
starts a drag regardless of y. This keeps the resize affordance
unambiguous even when the seam overlaps a valid row position.
- Otherwise, clicks inside `[1, seam - 1]` × `[header + 1, body_end - 1]`
(left-pane interior minus borders) convert to a row index via
`mouse.row - (header_height + 1)`.
- The index must be in `[0, sentinel_idx]`; beyond that we silently
drop the click. Row 0 = "Current directory", 1..=N = saved, N+1 =
"+ New workspace" sentinel.
- Clicks outside those ranges (header, footer, borders, right pane)
are ignored.
Layout heights are pulled from two new private consts mirroring
`render::render`'s `Constraint::Length(3)`/`Length(2)`. If the
chrome ever changes shape, both the render and hit-test paths need
updating together.
Double-click = launch is intentionally skipped: crossterm doesn't
emit native double-click events, so implementing it would need
tracking `(last_row, last_instant)` on `ManagerState` with a debounce
window. That's more state-machine than one item in this batch
warrants — left as a follow-up. Single-click-to-select is the
must-have and is fully wired.
Five new tests cover the happy path (row 0, mid-list row, sentinel
row), the negative path (header / borders / right pane / below
sentinel / footer), and the seam precedence regression guard.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): FileBrowser Esc steps back one level when not at root
Previously Esc always cancelled the modal. If the operator drilled into
a subfolder (via Enter on a plain dir or via the git-prompt
"Pick a subdirectory" path), a stray Esc collapsed the whole picker and
returned to the workspace list. Operators expect Esc to back out one
level — mirroring the behavior of `h` / `←` — and only cancel when
already at root.
Esc now:
- Clears any stale rejected_reason.
- Navigates one level up when cwd != root (sandbox-guarded, same as the
existing root-clamp).
- Cancels the modal only when cwd == root.
Git-prompt Esc is unchanged: it still dismisses only the prompt and
leaves the explorer open at the current cwd.
Footer hint updated from "Esc cancel" to "Esc up/cancel" (matching the
batch 16 uppercase-hotkey convention) — accurate for both drilled-in
and root cases.
Five new tests cover: esc-at-root cancels, esc-in-subfolder navigates
up, esc-three-levels-deep goes up exactly one, esc clears
rejected_reason, and the git-…
donbeave
added a commit
that referenced
this pull request
Apr 25, 2026
* docs(plans): workspace manager TUI implementation plan
Twenty-two-task TDD-shaped implementation plan for the workspace
manager TUI specced in #164. Ordered in seven phases:
1. Foundation — deps + module scaffolds + animation.rs refactor
2. Widgets — Confirm, TextInput, FileBrowser, WorkdirPick, PanelRain
3. State machine — ManagerState, EditorState, CreatePreludeState
4. Render — list view, editor (4 tabs), modal dispatcher
5. Input — modal-first key dispatch with per-stage routing
6. Integration — LaunchStage::Manager wire-in, m keybinding, full
editor + create key handling, ConfigEditor save/create/delete paths
7. Polish — style effects (boot reveal, save shimmer, toast expire),
integration test, final verification + PR
Each task is TDD-shaped: write failing test → run fails → implement →
run passes → commit. Complete code in every implementation step. No
placeholders.
Scope cut documented in self-review: tab-slider and panel-focus-glow
animations from the spec's Style section are omitted; they're cosmetic
and can land in a follow-up PR without rework.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): scaffold workspace manager modules + widget deps
Adds three ratatui ecosystem crates (ratatui-textarea 0.9,
ratatui-explorer 0.3, tui-widget-list 0.15) and enables ratatui's
unstable-widget-ref feature. Creates empty module structures at
src/launch/widgets/ and src/launch/manager/ to land typed setters,
widgets, and state transitions in subsequent commits.
No behavior change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(tui): extract render_rain_frame for reuse
Separates the per-frame rain rendering from digital_rain's event loop
so the upcoming PanelRain widget can render bounded-area rain without
duplicating the renderer. tick_rain and RainState become pub(crate)
for the same reason. Fullscreen digital_rain is rewritten to delegate
to render_rain_frame. No visible change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Confirm widget — Y/N modal
Hand-rolled Y/N confirmation dialog. Case-insensitive, Esc cancels.
~60 LOC + 5 tests. Used by delete-workspace and discard-changes flows.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): TextInput widget — single-line via ratatui-textarea
Wraps TextArea in single-line mode (intercepts Enter and Ctrl+M so
newlines are never inserted). Exposes a ModalOutcome<String> contract:
Enter commits, Esc cancels, everything else passes through to the
textarea for cursor / insert / backspace handling.
Cursor is placed at end of initial text on construction so editing
feels natural (backspace works immediately on prefilled values).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): FileBrowser widget — wraps ratatui-explorer
Folders-only filter, seeded from $HOME by default, adds 's' as
select-current-folder. Delegates all navigation (h/l/j/k/Enter/
Backspace/Home/End/PgUp/PgDn/Ctrl+h) to ratatui-explorer defaults.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): WorkdirPick widget — choice list via tui-widget-list
Derives the pick list from mount dsts + each ancestor up to /, with
labels (mount dst / parent / root). Deduplicates when multiple mounts
share ancestors. Enter commits selected path, Esc cancels.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): PanelRain widget — area-bounded phosphor rain
Wraps tui::animation's RainState engine for rendering into a bounded
Rect. Tick + render are separate so callers control frame rate.
Resizes state when the rect changes shape.
Adds RainState::new(cols, rows) constructor to animation.rs so the
widget can initialize state without duplicating the column/grid setup
that was previously inlined in digital_rain().
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): ManagerState + EditorState + CreatePreludeState types
Defines the top-level state machine per spec § 3: ManagerStage enum
(List / Editor / CreatePrelude / ConfirmDelete), EditorState with
dirty detection and change_count, CreatePreludeState with the
mounts-first wizard step enum, Modal enum with target enums, Toast
type, and constructors. Tests cover WorkspaceSummary derivation,
ManagerState::from_config, EditorState dirty detection, and
CreatePreludeState initial step.
ManagerStage and ManagerState carry a lifetime parameter propagated
from TextInputState<'a> (ratatui-textarea borrow). MountConfig lacks
Ord/Hash so change_count uses linear containment checks rather than
BTreeSet symmetric_difference.
Transitions and key handling are filled in by subsequent tasks.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): create-workspace wizard state transitions
Mounts-first flow: PickFirstMountSrc → PickFirstMountDst → PickWorkdir →
NameWorkspace. Each accept_* method advances the step. default_mount_dst
mirrors the host src path. default_name derives from the dst basename.
build_workspace assembles the final WorkspaceConfig.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): manager list view render
Renders ManagerStage::List: header banner, horizontal-split body
(workspace list + details pane), footer hint. Other stages rendered
by subsequent tasks (12: editor, 13: modal dispatcher).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): editor view render (all four tabs)
Renders Editor stage with General / Mounts / Agents / Secrets-stub
tabs, dirty markers on changed fields, save-count footer. Error
banner overlays the top of the tab body using --landing-danger
(#ff5e7a) for real errors.
Refactors top-level render to let stages declare whether they use
shared chrome (List, future ConfirmDelete) or their own full-screen
layout (Editor, future CreatePrelude).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): modal render dispatcher
Centers a modal Rect at 60x30 percent of the frame and dispatches to
the appropriate widget's render function based on the Modal variant.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): key dispatcher with modal precedence
Scaffolds handle_key with modal-first precedence: if a modal is open
anywhere in the state machine, events route to the modal handler
before per-stage handlers. Full editor + prelude wiring lands in
Tasks 16 / 17; this commit has stubs for those to keep the compiler
happy. List and ConfirmDelete stages are fully wired (navigation,
delete flow via ConfigEditor).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): LaunchStage::Manager + m keybinding
Adds a third launch stage and wires an m keypress from the Workspace
picker to transition into it. run_launch now takes AppConfig by value
+ &JackinPaths so the manager can open ConfigEditor. Footer hint in
the Workspace stage gains 'm manage'.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): editor key handling — tabs, save, discard, field edits
Implements Tab/Shift-Tab navigation between tabs, ↑↓ row selection,
Enter-to-edit (opens modal per field type), Space/* on Agents tab,
a/d on Mounts tab. s triggers save via ConfigEditor::edit_workspace
or create_workspace, with error banner on failure. Esc with pending
changes opens the Discard confirm modal; Esc with clean state returns
to the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): full create-workspace prelude flow
Chains the four modals (file browser → dst TextInput → workdir pick →
name TextInput) through CreatePreludeState. On completion, transitions
to Editor(mode=Create) with everything pre-populated. s in the editor
creates via ConfigEditor::create_workspace.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): manager style effects
Boot reveal on manager entry via tui::animation::digital_rain(400, None).
Save toast auto-expires after 3s. Shimmer: toast text flashes white during
the first 400ms post-show. JACKIN_NO_ANIMATIONS=1 disables the rain
transition.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* test(launch): end-to-end manager delete-workspace flow
Drives manager::handle_key with scripted key events (d, y). Asserts
the workspace is removed from on-disk config, the manager transitions
back to List, and the in-memory workspace list refreshes. Regression
guard against state-machine drift.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* style(launch): quiet clippy / fmt for workspace manager
Fix all clippy warnings introduced by the workspace-manager TUI (~1500
lines of new code): unnested or-patterns, collapsible-if, elidable
lifetimes, default-trait-access, items-after-statements, match-for-
equality, match-for-single-pattern, needless-pass-by-ref-mut,
doc-markdown (missing backticks), missing-const-for-fn, uninlined-
format-args, manual-Debug-non-exhaustive, large-enum-variant (allow),
too-many-lines (allow), unnecessary-trailing-comma, and the associated
fmt diff (11 files reformatted).
No logic changes; all tests pass (workspace_config_crud requires
--test-threads=1 due to pre-existing set_current_dir race).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): replace Workspace picker with manager as initial stage
The old Workspace picker stage is removed. LaunchStage::Manager becomes
the initial stage — jackin opens directly to the manager. Enter on a
workspace launches it (via the existing Agent picker); e opens the
editor; n creates; d deletes; q/Esc exits jackin. The m keybind is gone
— nothing to enter since we are already in the manager.
Esc from the Agent picker returns to the manager list (was: Workspace
picker, which no longer exists).
Also removes the mid-loop digital_rain(400, None) boot reveal that was
fighting with skippable_sleep's raw-mode toggling, which caused arrow
keys to print as raw escape sequences in the manager instead of being
captured by crossterm events.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): render active modals in manager
render_modal was defined but never called. Modals in Editor and
CreatePrelude stages transitioned correctly in state but had no
visible effect on screen — pressing n would silently put the user in
the create wizard with an invisible FileBrowser, making the create
and edit-field flows appear broken.
Also renders the ConfirmDelete variant's confirm modal directly
(ConfirmState on ConfirmDelete is a top-level field, not wrapped in
Modal::Confirm).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): modal footer hints + stage-aware manager footer
File browser and text input modals were rendering without any hint
about the keys to commit/cancel. Users opening the create workspace
flow saw the folder picker but couldn't progress — pressing Enter
only descended into folders (s is the select key for ratatui-explorer),
and there was no visual cue about the right key.
Adds a one-line phosphor-dim italic footer inside each modal:
- FileBrowser: ↑↓ navigate · Enter open · h/← up · s select · Esc cancel
- TextInput: Enter confirm · Esc cancel
Also makes the top-level manager footer hint stage-aware:
- List stage: existing navigation hint (unchanged)
- CreatePrelude: Create workspace · follow the prompts · Esc cancel
- ConfirmDelete: Y yes · N no · Esc cancel
- Editor: still delegates to render_editor's own footer
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): Esc in create wizard returns to list immediately
Previously pressing Esc inside the first create-wizard modal cleared
the modal but left the state machine stuck in ManagerStage::CreatePrelude
with no modal active — render drew a blank body, requiring a second Esc
to reach the non-modal prelude handler that transitions back to List.
Now the post-modal check distinguishes three outcomes: in-progress
(modal still open), complete (wizard finished with name), and
cancelled (modal cleared without a name). Cancelled transitions to
List in the same input pass.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): wire rename + render all agents on Agents tab
Fixes two spec gaps:
1. Agents tab rendered only currently-allowed agents, so the user
couldn't add agents via Space toggle — non-allowed agents were
invisible. Now iterates config.agents (the full set) and shows
[x] or [ ] per agent based on pending.allowed_agents membership.
Threads &AppConfig through manager::render → render_editor →
render_agents_tab. Also fixes set_default_agent_at_cursor to use
config.agents for cursor-to-agent resolution instead of the
allowed-only list.
2. Workspace rename was a TODO. Now:
- ConfigEditor gains rename_workspace(old, new) using toml_edit's
key-rename (preserves nested tables + array-of-tables). Rejects
empty new name, collision, and missing old name.
- General tab's name row is editable on Enter (in Edit mode) via
TextInput modal.
- apply_text_input_to_pending stashes the name on
EditorState::pending_name.
- save_editor calls rename_workspace before edit_workspace when
pending_name differs, then updates editor.mode so subsequent saves
target the new name.
- change_count + is_dirty + render dirty marker all track the rename.
Tests: three new unit tests on ConfigEditor::rename_workspace covering
happy path (nested tables preserved), collision rejection, and empty-
name rejection.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): FileBrowser s commits cwd, not highlighted entry
Pressing s in an empty or file-only folder previously committed the
highlighted entry, which in such a folder is '../' — so the user got
the parent directory, not the folder they were viewing. This was
especially bad for newly-created empty workspace source folders.
Now s commits the explorer's current working directory via
FileExplorer::cwd() (ratatui-explorer 0.3.x). User intent is preserved:
'I've navigated to this folder — select it.' Footer hint updated from
's select' to 's use this folder' to reflect the semantics.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): render active-row cursor in editor tabs
EditorState::active_field tracked the cursor but render functions
didn't display it — users couldn't tell which row Enter / Space / * /
a / d would target. Add a ▸ prefix and phosphor-green bold to the
selected row across all three tabs (General, Mounts, Agents).
Also clamp Down-arrow to the last valid row so the cursor can't run
off the end of the visible content, and thread &AppConfig through to
handle_editor_key's Down handler so it can size the Agents tab's
row count correctly.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): UX polish pass on manager create/edit flows
Nine polish fixes reported after live walkthrough:
1. TextInput/Confirm modals were 30%-of-screen tall, rendered as big
empty boxes around a single line. Now variant-aware: inputs/confirms
are 5-6 rows fixed; file browser and workdir pick stay taller for
their scrolling lists.
2. 'last used' row hidden in Create mode (no history exists).
3. 'default agent' row hidden in Create mode (no agents picked yet).
4. Footer hint is now row-contextual: 'Enter rename' on name row,
'Enter pick workdir' on workdir row, 'a add / d remove' on mounts,
'Space toggle / * set default' on agents, nothing on read-only
rows. Base hint says 's save workspace' (was 's save') for clarity.
5. File browser gets a prominent outer block titled '<cwd> · press
[S] to use this folder' — the select affordance was previously
buried in a dim footer line.
6. Mount rows collapse 'src → dst' to just 'path' when src == dst
(host-path-mirror default — redundant arrow gone).
7. Mounts tab '+ Add / − Remove selected' footer uses white-bold for
the action words to distinguish from the mount list.
8. Agents tab gets a top banner clarifying empty = 'all allowed'
semantics vs non-empty = custom allow-list.
9. Read-only rows (last used) no longer advertise Enter in the footer.
Also fix max_row_for_tab in input.rs: Create mode General tab only
has 2 rows (name read-only + workdir), not 4.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): Confirm modal shows Y/N as styled buttons
Previously the modal rendered '[Y]es · [N]o (default) · Esc cancel' as
inline text inside a tall 30%-of-screen box, which looked like a
multi-line textarea. Now:
- Modal is compact (6 rows)
- Yes/No render as inverted-video buttons, centered
- No (default) uses white-on-black to distinguish as default action
- Esc cancel moves to a dim italic footer hint at the bottom
- Prompt text stays bold-white at the top
Enter intentionally unbound — destructive confirms should not commit
on accidental Enter presses.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): remove nested borders in FileBrowser modal
ratatui-explorer's widget renders its own bordered block with the CWD
as title. The prior polish pass added an outer block with 'press [S]
to use this folder' — the result was double borders, ugly and
confusing.
Drop the outer block. Show the 'press [S]' affordance as a bold-white
centered line ABOVE the explorer (no border), and keep the dim
navigation hint as a line BELOW. The explorer's own cwd-titled block
stays — no nesting.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Confirm modal gains Tab focus + Enter commits focused
Adds standard confirmation-dialog UX: Tab / Shift+Tab / ←→ / h/l cycle
focus between Yes and No; Enter commits the focused button. Default
focus is No (destructive action protection — accidental Enter won't
commit Yes). Y/N direct shortcuts still work regardless of focus.
Visual: focused button gets white bg + black text + bold; unfocused
gets phosphor-green bg. Footer hint updated to mention Tab + Enter.
Modal grows from 6 to 7 rows for a second spacer between buttons
and hint (was visually cramped).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): shrink FileBrowser + WorkdirPick modal heights
Prior sizing was 60 rows for FileBrowser and 40 rows for WorkdirPick —
effectively fullscreen on a typical 40-50 row terminal. Tight 20 and
12 rows fit comfortably and still show enough entries without the
modal swallowing the whole screen.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): + Add mount is a selectable sentinel row
Mirrors the + New workspace sentinel in the manager list. The Mounts
tab now renders + Add mount as a real selectable row at the end of
the list, selected via ↑↓, activated via Enter. Visual treatment is
white bold (distinguishing it from the green mount rows).
- max_row_for_tab reports len() (mount count + sentinel index) for
Mounts so ↓ can reach the sentinel.
- remove_mount_at_cursor is a no-op on the sentinel (guard already existed).
- a (anywhere on the tab) still works as a quick-add shortcut.
- Contextual footer hint differentiates between 'on a mount row'
(d remove · a add) and 'on the sentinel' (Enter add · a add).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Agents tab shows [all] / [custom] status badge
Replaces the implicit 'empty list = all allowed' with an explicit
status line at the top of the Agents tab:
Allowed agents: [ all ] (when allowed_agents is empty)
Allowed agents: [ custom ] (3 of 5 allowed) (when non-empty)
The badge is an inverted-video token (phosphor-green bg for 'all',
white bg for 'custom') making the current mode immediately visible.
The agent list below stays as a checklist — toggling updates the
status badge live.
Cursor semantics also shift: cursor is now 0-based into config.agents
(no more header-offset-by-one). toggle_agent_allowed_at_cursor and
set_default_agent_at_cursor are updated accordingly. max_row_for_tab's
Agents arm drops to len()-1.
set_default_agent_at_cursor now also auto-allows the agent being set
as default (was previously a no-op if the agent wasn't already in
allowed_agents).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Mounts tab shows folder / git · <branch>
Adds a mount_info helper that inspects the host-side src path on
render: checks for .git as dir or submodule-gitfile, reads HEAD, and
reports the current branch (or detached short-sha). Renders next to
each mount row as dim italic metadata:
/Users/…/repo (rw) · git · main
/Users/…/scratch (rw) · folder
/Users/…/gone (rw) · missing
Six unit tests cover: missing path, plain folder, normal repo with
branch, detached HEAD, submodule .git file, label formatting.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Save/Discard/Cancel modal + richer details pane
Two UX upgrades:
1. Exit-with-changes now offers three explicit choices instead of
binary 'Discard Y/N'. New SaveDiscardCancel modal with three
buttons (Save / Discard / Cancel), Tab cycles focus, Enter commits
the focused option. S/D/C/Esc shortcuts work regardless of focus.
Default focus is Cancel (safest). Save intent triggers ConfigEditor
save → list; Discard just drops pending; Cancel keeps the editor.
2. Manager list's details pane now shows the full mount list (with
folder / git · <branch> labels, same as the Mounts tab) and the
allowed-agents list (or 'any agent' when unrestricted). Title drops
the duplicate workspace name since the list selection already shows
it.
5 new unit tests on SaveDiscardState covering focus cycling and key
shortcuts.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): restrict FileBrowser to \$HOME, rename main title
Four UX fixes:
1. Main manager screen title 'manage workspaces' → 'workspaces'
(the screen does more than manage — launch, create, edit, delete).
2. FileBrowser modal goes fullscreen (100% x 100%) so the main chrome
doesn't peek through and confuse the visual.
3. FileBrowser now:
- Starts at \$HOME (already did)
- Excludes Library, Applications, Movies, Music, OrbStack, Pictures
from the listing via filter_map
- Clamps cwd back to \$HOME if the user escapes above it via
set_cwd() (ratatui-explorer 0.3.x has this method)
- Rejects \$HOME itself as a workspace source
- Rejects ~/.jackin/* (jackin's reserved data area)
4. Rejected selections show an inline red error banner
(#ff5e7a) above the explorer. Cleared on next keypress.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): display paths as ~/… via shorten_home
Paths starting with $HOME now render as '~/...' in the TUI:
General tab workdir, Mounts tab rows, details pane mounts/workdir,
WorkdirPick choices. Consistent with jackin's existing shorten_home
helper (already used elsewhere in the launcher).
Paths stored on disk are unchanged — this is display-only.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): mount table formatting + FileBrowser resize/colors
Two fixes:
1. Mount lists in the details pane and Mounts tab render as an
aligned 3-column table (path, mode, type) instead of a free-form
line where the '(rw)' tag and type metadata floated at variable
positions. shorten_home applied to paths consistently via the
shared format_mount_rows helper, which is called from both
render_details_pane and render_mounts_tab.
2. FileBrowser modal goes from fullscreen (100%) to 70%x70%, letting
the surrounding chrome show again so the dialog reads like a
dialog, not a whole screen. Theme configured to use jackin's
phosphor palette (green text, bright-phosphor highlight, shortened
CWD title via shorten_home in a dynamic with_title_top closure).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): drop Agents 'default' column + cap workspace list height
1. Agents tab header 'allowed? · default · agent' → 'allowed? · agent'.
The star marker next to the agent name already indicates default;
the dedicated column was empty for every non-default row.
2. Manager list body now caps at content height (workspace count + 2
border rows + 1 sentinel row) instead of filling the whole frame.
5-6 workspaces no longer render in a box that looks two-thirds
empty.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* Revert "fix(launch): cap workspace list height to content"
The height cap made the space below the boxes visibly empty, which
reads worse than the previous full-height boxes. User feedback:
'before it was better when it was using the whole vertical space.'
Keeps the Agents tab header change from the same original commit
(3fdab9f3) — only the list-body sizing is reverted.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): hide FileBrowser .. entry at $HOME root
Previously the '../' entry was always shown in the file browser.
When the user was at $HOME, selecting it would escape the sandbox
(and was then clamped back by set_cwd) — confusing and cluttered.
Now the filter hides '..' when its target path is outside the root
subtree. At $HOME the entry disappears; at any subfolder of $HOME
it still appears so the user can navigate back up.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): hide empty right pane, split details, clickable git links
Three UX improvements:
1. When the cursor is on '+ New workspace' in the manager list,
the right details pane is hidden entirely — the list takes full
width. No more empty bordered box for the sentinel row.
2. Details pane split into three stacked sub-panels: General (workdir
+ last used), Mounts (tabular with header row), Agents (list or
'any agent'). Each has its own bordered mini-block with phosphor-
dark border and white-bold title. The outer 'Details' block is gone.
3. Git branch URL resolution wired up: inspect() now parses
<git_dir>/config to find the origin remote and derives a web URL
(GitHub, GitLab, generic HTTPS/SSH). MountKind::Git gains a
web_url: Option<String> field; MountKind::labeled_hyperlink() wraps
the branch name in OSC 8 escape sequences for supported terminals
(iTerm2, kitty, WezTerm, Alacritty, modern Terminal.app).
OSC 8 fallback: ratatui's Paragraph widget strips raw ESC bytes, so
the render path continues to call label() (plain text). The
hyperlink infrastructure (labeled_hyperlink, osc8_link, web_url) is
retained for a future raw-terminal-write path. Both are annotated
#[allow(dead_code)] with an explanatory TODO.
5 new unit tests on remote-URL parsing (GitHub SSH, GitHub HTTPS,
ssh:// protocol, GitLab SSH, config-file parse). All 566 tests pass.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): polish FileBrowser, WorkdirPick, and mount-dst prompt
- FileBrowser entries now render white instead of phosphor-green so the
bright-green highlight is the unambiguous focus indicator.
- TextInput prompts for mount destination say "destination (default:
same as host path)" instead of the internal "Mount dst" phrasing.
- WorkdirPick lines are laid out as a table: the path column is padded
to the widest choice so the dim+italic label column (`(mount dst)`,
`(parent)`, `(root)`, `(home)`) lines up cleanly.
- WorkdirPick filters `/` and the literal parent of `$HOME` (e.g.
`/Users` on macOS, `/home` on Linux) from the choice list — those
paths are never useful workdir targets.
- When a path is exactly `$HOME`, label it `(home)` instead of
`(parent)` so the workspace operator sees a recognisable name.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): FileBrowser s commits highlighted folder
Previously `s` always committed the explorer's cwd, which meant the
operator had to press Enter to navigate into the target folder before
committing — even though the folder was already highlighted and the
target of a single Enter press.
Reading `FileExplorer::current()` lets us commit the highlighted entry
directly when it is a real child directory. The synthetic `../`
parent-link row and the empty-listing case both fall back to the cwd,
preserving the previous behaviour for those edge cases.
The existing $HOME and `~/.jackin/*` rejection rules apply to whichever
path is chosen as the commit target.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): keep General-tab labels white; note agent-hyperlink TODO
- render_editor_row and render_editor_readonly_row no longer shift the
label column to phosphor-green when the row is focused. Labels stay
white (bold when focused); values keep their phosphor colouring for
editable rows and dim phosphor for read-only rows.
- Read-only rows used to render everything in phosphor-dim, which made
the editor view look washed-out. They now match the editable-row
label treatment (white) with a dim value + italic "(read-only)"
suffix, giving the operator a cleaner signal-to-noise ratio.
- Added a TODO in render_agents_subpanel mirroring the existing
labeled_hyperlink() note in render_mounts_subpanel: ratatui's
Paragraph strips OSC 8 ESC sequences, so agent-name → GitHub links
stay plain-text until a raw-write path exists.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): gate editor save on mount-collapse plan
The editor used to write configs straight to disk via
`ConfigEditor::edit_workspace` (or `create_workspace`), which meant the
operator could save a workspace with overlapping mounts like
`~/Projects` and `~/Projects/test`. The CLI rejects this unless you
confirm or pass `--prune`; the TUI now does the same.
Flow:
- On `s`, run `workspace::planner::plan_edit` (Edit) or `plan_create`
(Create) against the pending mount set.
- `CollapseError::{ReadonlyMismatch, ChildUnderExistingParent}` ->
error banner, no write.
- Pre-existing collapses only (no edit-driven) -> error banner
referencing `jackin workspace prune <name>`. The operator can't fix
these from the editor alone and the CLI prune command already exists
for this case.
- Edit-driven collapses -> open a `Modal::Confirm` with a
`ConfirmTarget::SaveCollapse` target, listing each child/parent pair
in the same wording as the CLI. On Yes, the save re-enters with
`EditorState::collapse_approved = true` and commits the collapsed
mount set via `plan.effective_removals` / `plan.final_mounts`. On No
/ Esc, pending mounts are kept intact so the operator can edit by
hand.
Pattern: a boolean flag on `EditorState` + a new `ExitIntent::RetrySave`
variant so the confirm-yes path reuses the existing modal-exit routing
but stays in the editor on success (rather than bouncing to the
workspace list, which is what `ExitIntent::Save` does). The plan
itself is not stashed; it is cheap to recompute on re-entry.
The `Confirm` widget now grows its prompt region to match the number
of lines in `state.prompt`, and `render_modal` sizes the outer rect
via `confirm::required_height` so multi-line collapse summaries render
without clipping.
Tests (5 new):
- `save_editor_opens_confirm_on_edit_driven_collapse`
- `confirming_collapse_writes_collapsed_set`
- `cancelling_collapse_keeps_pending_mounts_intact`
- `readonly_mismatch_produces_error_banner_no_write`
- `pre_existing_collapse_produces_prune_error_banner`
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): structured footer with per-item styling
Introduce a `FooterItem` enum (Key / Text / Dyn / Sep / GroupSep) and a
shared `render_footer` that emits spans with a consistent palette:
- Key glyphs (↑↓, Enter, e/n/d/q, Tab, Esc, S, Y/N, *, Space) render in
WHITE + BOLD so they pop out of the legend.
- Action labels ("launch", "edit", "new", …) render in PHOSPHOR_GREEN.
- Inline dots (·) render in PHOSPHOR_DARK as a faint separator.
- A GroupSep (three spaces, no style) introduces a wider visual gap
between logical groups — navigation, per-row actions, and exit.
Migrate every footer call site to this scheme:
- `manager/render.rs` List / CreatePrelude / ConfirmDelete / Editor
footers build `Vec<FooterItem>` explicitly so the grouping is
deliberate per stage.
- Agent-screen footer in `launch/render.rs` uses the same inline spans.
- Modal-local hints inherit the scheme (file_browser navigation + "[S]
to use this folder" affordance, text_input "Enter confirm · Esc
cancel", confirm "Tab cycle · Enter confirm · Y yes · N no", and
save_discard).
Add unit tests covering the span-style mapping per variant plus
smoke tests for the List and ConfirmDelete stage footers.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): keep right pane visible on '+ New workspace' sentinel
Batch 7 expanded the list to full width when the cursor landed on the
sentinel row. The operator wants the 45/55 split preserved — the layout
should not shift as the cursor moves — with the right pane rendered as
an empty bordered block (same PHOSPHOR_DARK border as the General /
Mounts / Agents sub-panels) when there is no workspace to describe.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align mount-table header with data columns
The mount-table header was a hardcoded string (" path<23 spaces>mode<3
spaces>type") while data rows computed their path column width
dynamically from the widest row. When paths were shorter than 23 chars
the header appeared drifted relative to the data; when they were longer
the header's "mode" column collided with the data's mode column at a
different offset.
Share the column-width computation between the header and data rows:
- Extract `mount_path_width` which returns max(row_path, "path".len(),
10) so the header and data always use the same column boundary.
- Add `render_mount_header(path_w)` that uses the same format string as
the data rows, then have both the read-only details subpanel and the
editor Mounts tab consume it.
- Pin the `mode` column to a shared `MOUNT_MODE_COL_WIDTH = 4` constant
(covering "mode" as well as "rw"/"ro" + trailing space) so it no
longer over-pads inconsistently.
Add unit tests that build mount rows with mixed path lengths and assert
the header's "mode" column starts at the same character index as each
data row's "mode" column.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): describe workspace concept on '+ New workspace' pane
Replace the empty bordered block shown to the right of the manager list
when the sentinel row is focused with a two-panel description pulled
from the "What is a workspace?" / "Why save a workspace?" sections of
the workspaces guide. Keeps the right-hand real estate useful for
first-time operators and matches the General/Mounts/Agents sub-panel
chrome for visual consistency.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): restore 'Current directory' row in workspace manager
Before the TUI redesign the launcher's first row was a synthetic
"Current directory" choice that let operators launch an agent against
cwd without saving a workspace. The manager's rewrite dropped it; this
reinstates it as row 0 of the list with the right-pane summary, the
cwd-aware preselect, and the launch wiring that matches the old
behaviour.
Row layout (enforced by ManagerState::from_config, render_list_body,
and handle_list_key):
row 0 → synthetic "Current directory"
rows 1..=N → saved workspaces
row N+1 → "+ New workspace" sentinel
Edit (`e`) and Delete (`d`) are rejected on row 0 with a toast. Enter
on row 0 emits a new InputOutcome::LaunchCurrentDir; the run-loop
routes it through the same agent-picker transition as LaunchNamed,
reusing LaunchState::workspaces[0] (the CurrentDir choice built by
LaunchState::new). Preselect reuses find_saved_workspace_for_cwd so
TUI and CLI agree on "which workspace am I in?".
The right pane branches on row 0 → render_current_dir_details_pane
(dedicated renderer; no last-used row, no edit affordance, "any
agent"). The sentinel description pane lands in the same commit's
sibling already; saved-workspace rows continue to use the shared
render_details_pane with `workspaces[selected - 1]`.
Tests added:
- manager_preselects_saved_workspace_matching_cwd
- manager_preselects_current_directory_when_no_saved_matches
- manager_current_directory_is_first_row
- current_directory_row_rejects_edit_and_delete
- enter_on_current_directory_returns_launch_current_dir
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): polish mount-header gap, modal titles, and FileBrowser size
- Mount table header: add two-space gutter between `mode` and `type`
so the header no longer reads "modetype". Data rows now emit the
matching two-space gap so the `type` column aligns in both the
read-only Mounts subpanel and the editor Mounts tab.
- Text-input + Workdir-pick modal block titles render WHITE + BOLD to
match the General/Mounts/Agents block titles on the main screen.
Confirm + SaveDiscard already use WHITE+BOLD — left untouched.
- WorkdirPick path values render WHITE (the `(mount dst)`/`(parent)`/
`(home)`/`(root)` label suffix stays PHOSPHOR_DIM italic).
- FileBrowser modal height drops from 70 absolute rows to 22 so it
no longer eats the whole screen. Width stays at 70%.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): classify git mounts by host and relabel GitHub remotes
- Introduce `GitHost { Github, Other }` on `MountKind::Git` so the
render path can tell which remotes have an "open in browser"
affordance. `inspect` populates this from `parse_remote_origin_url`:
SSH `git@github.com:`, HTTPS `https://github.com/…`, and
`ssh://git@github.com/…` all resolve to `Github`; anything else
(self-hosted, GitLab, no remote, unparseable URL) falls through to
`Other`.
- `remote_to_web` now returns `Some(url)` only for GitHub hosts and
`None` for everything else — it no longer synthesises `gitlab.com`
URLs. Non-GitHub remotes keep `web_url: None` on the `MountKind`.
- `MountKind::label()` renders `github · {b}` / `github · detached {sha}`
/ `github` for GitHub hosts and keeps the generic `git · …` prefix
for `Other`. `MountKind::Folder` / `Missing` unchanged.
- `remote_to_web_gitlab` test re-purposed to assert GitLab (and other
non-GitHub hosts) now return `None`. New tests for the GitHost split
via `inspect` and for the `remote_points_at_github` predicate covering
all three URL forms + a GitHub-lookalike subdomain rejection.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): 'o' key opens highlighted GitHub mount in the browser
- Add the `open` crate (5.x) so the editor can launch the system
browser without blocking the TUI (`open::that_detached`).
- Wire `o` into the editor's Mounts tab: when the cursor is on a
mount row whose source resolves to a GitHub-hosted repo with a
web URL, pressing `o` opens that URL in the operator's default
browser. Non-GitHub / folder / missing mounts emit an "no GitHub
URL for this mount" toast so the hint is discoverable; the sentinel
"+ Add mount" row is a silent no-op.
- `contextual_row_items` now composes an `o open in GitHub` item
onto the existing `d remove · a add` pair when the current row is
a GitHub mount. List-view mounts pane is unchanged — the `o` key
only binds in the editor.
- Tests: `github_mount_row_includes_open_in_github_hint` and
`non_github_mount_row_omits_open_in_github_hint` pin the footer
composition. No unit test for the browser side-effect itself.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): flag git repos in FileBrowser and offer mount-or-dive prompt
Part A — directory listing:
- `annotate_file` (new filter_map body) stats each directory for a
`.git` child and appends U+25A3 (▣) to the display name when present.
Works for plain clones (`.git` is a directory) and submodules (`.git`
is a file containing `gitdir: …`). Single stat per entry — no
recursive walk.
Part B — Enter on a git-repo row:
- New `GitPromptFocus { MountHere, EnterIn, Cancel }` + two fields on
`FileBrowserState` (`pending_git_prompt`, `pending_git_focus`) drive
an in-widget confirm overlay. Enter on a git-repo row opens the
prompt; Tab/←→/h/l cycle focus; Enter commits the focused option;
M/E/C are direct shortcuts; Esc dismisses the prompt without
cancelling the browser.
- MountHere commits the repo path through the same sandbox rules as
`s` (rejects root / `~/.jackin/*`). EnterIn navigates into the repo
via `explorer.set_cwd` (avoids re-posting Enter, which would re-open
the prompt). Cancel just clears state. Non-git folders keep their
usual Enter-navigates-in behavior.
- Overlay renders as a centred 3-button bar inside the explorer area
so the listing stays visible as context. Phosphor palette + focus
styling mirrors `confirm.rs`/`save_discard.rs`; button ring copied
locally rather than cross-importing between widgets.
- Footer legend swaps to `Tab cycle · Enter confirm · Esc cancel`
while the prompt is active.
Tests cover: marker on `.git`-dir and submodule-`.git`-file cases, no
marker on plain folder, Enter opens prompt on repo row, MountHere
commits the path, EnterIn navigates in and clears prompt, Cancel
clears without cwd change, Esc dismisses prompt without cancelling
browser, plain-folder Enter navigates as before, and the `M` shortcut
commits regardless of current focus.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): add mount-destination choice modal widget
Introduces the `mount_dst_choice` widget and wires a new
`Modal::MountDstChoice` variant into the manager's modal enum plus
render dispatcher. No input behaviour changes yet — follow-up commits
swap the Editor and Prelude FileBrowser→TextInput chains to route
through this modal.
The widget is the 3-button focus-ring pattern pioneered by
`save_discard`: default focus on `OK`, Tab/BackTab cycling, and single-
letter shortcuts (`o`/`e`/`c`). Default on `OK` because the common case
is `dst = src`, so an accidental Enter commits that without surprise.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): route editor add-mount through destination choice modal
The FileBrowser→TextInput chain in the Editor's Mounts tab assumed
every operator wanted to edit the destination path. In practice, 95%
of mounts commit with dst = src. Swapping in the new MountDstChoice
modal makes the common path a single Enter press and keeps the old
behaviour one keystroke away via `Edit destination`.
`apply_file_browser_to_editor` now opens MountDstChoice instead of
pushing a provisional mount plus TextInput. The actual push happens
in the MountDstChoice commit handler:
- OK: push MountConfig { src, dst = src, rw }, close modal.
- Edit destination: push the provisional mount (as today) and open
Modal::TextInput{MountDst} pre-filled with src. The existing
TextInputTarget::MountDst handler overwrites the provisional dst.
- Cancel / Esc: close the modal, leave pending.mounts untouched.
Behavioral tests pin all three paths and guarantee no mount is
pushed until the operator commits in the choice modal.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): route create-prelude add-mount through destination choice
Mirrors the editor-side change: the Create wizard's FileBrowser step no
longer assumes the operator wants to edit the destination. Instead, the
prelude now opens MountDstChoice after FileBrowser commits, offering
the fast `OK` path that skips TextInput entirely.
Both paths (OK and TextInputDst commit) share a new helper
`prelude_advance_to_workdir_pick` so the downstream WorkdirPick stage
receives the same staged mount regardless of whether the operator
edited the destination. This keeps the chain FileBrowser → (choice) →
WorkdirPick → TextInput(Name) intact for the `OK` shortcut.
Cancel on MountDstChoice matches today's Esc-during-TextInput
behaviour: close the modal, leave the prelude state alone so the
outer dispatcher treats it as a wizard-cancellation and returns to
the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): extract mount-dst-choice dispatch to keep clippy happy
The inline `Modal::MountDstChoice` arm inside `handle_editor_modal`
pushed the function above clippy's 100-line ceiling. Extract the
outcome dispatch into `dispatch_editor_mount_dst_choice` and tidy the
helper's doc comment so `TextInput` doesn't trip the missing-backticks
lint. No behavioural change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): strip trailing slash in FileBrowser name filter
ratatui-explorer appends `/` to directory names at runtime, so the
filter in `annotate_file` was comparing `"Library"` against `"Library/"`
and silently letting every excluded entry render. Same bug let `..`
through on the sandbox-escape check. Normalize with
`trim_end_matches('/')` before matching, and harden the `s`/Enter
paths + default key dispatch to guard against empty listings (the
fixed filter can now produce an empty `files()` which made
`current()` and nav-key dispatch panic inside ratatui-explorer).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* polish(launch): simplify Destination modal title
Rename the mount-destination TextInput label from
`destination (default: same as host path)` to plain `Destination`.
The parenthetical hint is redundant after batch 12: the TextInput
only opens when the operator explicitly picks "Edit destination"
on the MountDstChoice modal, so they're already in deliberate-edit
mode with the src pre-filled as the default.
Also capitalizes the title to match the other modal block titles
(Confirm, Unsaved changes, Mount destination, Git repo detected,
Workdir pick, Rename workspace, Name this workspace). No other
titles needed changes — the audit was clean.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): bind Left/Right to prev/next tab in Editor
Extend `handle_editor_key` so Right matches Tab (forward cycle) and
Left matches BackTab (reverse cycle). Wrap-around behavior mirrors
the existing Tab contract: General → Mounts → Agents → Secrets →
General, and symmetrically for reverse.
Modal-open precedence is already guarded by the early-return in
`handle_key` — Left/Right continue to feed into modal handlers
(Confirm, SaveDiscard, MountDstChoice) when a modal is active.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): wire list-view `o` to open workspace GitHub mounts
Extend the `o` key beyond the Editor's Mounts tab to the workspace
list view. On a saved workspace row:
0 GitHub mounts → toast "no GitHub URLs for this workspace"
1 GitHub mount → open::that_detached immediately
≥2 GitHub mounts → open a new GithubPicker modal; Enter commits
the highlighted URL to open::that_detached.
Row 0 (Current directory) and the `+ New workspace` sentinel toast
`no workspace selected` for discoverability. The list footer now
surfaces `o open in GitHub` only on rows whose workspace resolves
to ≥1 GitHub-hosted mount.
Adds:
- new `widgets/github_picker.rs` widget (title-styling and tab-list
pattern mirror WorkdirPick so the modal feels native);
- `Modal::GithubPicker { state }` variant, with arms closed in the
render-size switch, `handle_editor_modal` (defensive cancel), and
a new `handle_list_modal` dispatcher;
- `list_modal: Option<Modal<'a>>` slot on ManagerState — list-view
modals weren't previously anchored anywhere; Editor/CreatePrelude
keep their per-stage modal slots unchanged;
- `resolve_github_mounts_for_workspace` helper, shared by the input
handler and the render-side footer-hint guard.
Piggybacks a one-line clippy fix in file_browser.rs
(`iter().any(|x| *x == bare)` → `EXCLUDED.contains(&bare)`) that
surfaced after the trailing-slash filter landed.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): drop cwd suffix from Current directory row label
The list row for the synthetic "Current directory" choice used to read
`Current directory (~/Projects/foo)`. The right-pane details already
show the cwd on the `workdir` line, so the parenthetical suffix is
duplicate visual load. Render just `Current directory`.
Row 0 keeps its WHITE colour so the synthetic choice still visually
separates from the phosphor-green saved workspaces below it.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align mount-table type column with header
MOUNT_MODE_COL_WIDTH was 2, matching the literal width of rw/ro but
leaving a 2-char gap before the data row's kind column versus the
header's 4-char "mode" label. Header and data rows shared the same
"{mode:<mw} type" format string but MOUNT_MODE_COL_WIDTH no longer
matched the header label length, so `type` and its data (e.g. "folder")
rendered at different offsets.
Pin MOUNT_MODE_COL_WIDTH to 4 so rw/ro pad to the header's "mode"
width. Both the header and data emit the same two-space gutter before
the `type` column, so the kind label lines up with the header offset.
Extend the existing gap-between-mode-and-type test to additionally
assert that `header.find("type") == data.find("folder")` — the
type-column offset must match for a row with a plain folder mount.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): drop dead OSC 8 hyperlink scaffolding
The `o`-opens-in-system-browser path (via open::that_detached)
supplanted the aspirational OSC 8 hyperlinks-in-terminal route. The
OSC 8 helpers were already #[allow(dead_code)] — remove them:
- `osc8_link()` — wrapped text in OSC 8 ESC sequences;
- `MountKind::labeled_hyperlink()` — built a GitHub-linked label
from a branch/sha + url;
- their associated NOTE blocks on render.rs (mounts subpanel) and
the agents-hyperlink TODO next to render_agents_subpanel.
The `web_url: Option<String>` field stays — the `o` key consumes
it to open the branch URL. Likewise `remote_to_web`,
`parse_remote_origin_url`, and the `GitHost::Github` classification
are all still in the live path.
Clippy baseline (4) unchanged; no tests touched.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): mouse-draggable list/details divider
Add a draggable seam between the workspace list pane and the details
pane in the manager TUI. Click-and-drag on the seam column (within ±1)
resizes the split; the percentage is clamped to [20, 80] so neither
pane can be starved.
Mechanically:
- ManagerState gains `list_split_pct: u16` (default 45) and
`drag_state: Option<DragState>`. `clamp_split` + split-range consts
live alongside. `render_list_body` reads `list_split_pct` instead
of the hard-coded 45/55.
- `src/launch/mod.rs` enables `EnableMouseCapture` after entering the
alternate screen and `DisableMouseCapture` in the terminal guard's
Drop. Side-effect: the terminal's native click-drag text selection
stops working while the TUI is running — hold Shift (Terminal.app,
iTerm2) or Option (iTerm2) to bypass. Documented inline.
- The run-loop now matches on `Event::{Key, Mouse, _}` (was a bare
`if let Event::Key`). Mouse events in the Manager stage dispatch
to a new `manager::input::handle_mouse` with the current terminal
size as a `ratatui::layout::Rect`.
- `handle_mouse` hit-tests the seam, captures a `DragState` anchor
on `Down(Left)`, updates `list_split_pct` on `Drag(Left)`, and
clears the anchor on `Up(Left)`. It also gates on List stage, no
open list-modal, and `term_size.width >= 40`.
Unit tests (8 new, pure state manipulation — no ratatui loop):
- mouse_down_on_seam_starts_drag
- mouse_drag_updates_split_pct
- mouse_drag_clamps_to_min_and_max
- mouse_up_ends_drag
- mouse_down_far_from_seam_does_not_start_drag
- drag_ignored_when_list_modal_open
- drag_ignored_on_non_list_stage
- drag_ignored_when_terminal_too_narrow
Clippy baseline (4) unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): rename current-dir pane first block to "General"
The synthetic "Current directory" row has a right-pane first block titled
" Current directory ", but the left-list row label already conveys that
context. Rename to " General " to match the saved-workspace details pane
(General / Mounts / Agents) so both panes use the same three sub-panel
titles.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): remove phantom empty row in current-dir Mounts block
`render_current_dir_details_pane` hard-coded the Mounts block at
`Constraint::Length(5)`, which over-allocated by one row for the
single-mount current-directory case and left a visible empty line
inside the block border.
Extract the height formula from `render_details_pane` into a shared
`mount_block_height` helper (2 borders + 1 header + max(1, N) data rows,
clamped to 12) and use it from both pane renderers so the two paths
produce identically-tight Mounts blocks.
Covered by four regression tests pinning the formula for the empty,
single, multi, and many-mount cases.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align General sub-panel content with Mounts/Agents
The General sub-panel (on both the saved-workspace pane and the
current-directory pane) rendered its `workdir`/`last` rows flush against
the block's left border, while the Mounts and Agents sub-panels already
used a two-space indent. The mismatch gave the right pane a jagged left
edge across the three stacked blocks.
Add the same two-space prefix to the General rows on both panes. The
convention is pinned by a new `SUBPANEL_CONTENT_INDENT` constant and
two visual regression tests that render each sub-panel to a
`TestBackend` buffer and assert the first visible character of row 0
sits at that indent relative to the block's left border. Covers the
"any agent" fallback and the starred-default-agent row explicitly.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): follow worktree commondir for GitHost detection
Git worktrees are checkouts whose `.git` is a file pointing at
`<main>/.git/worktrees/<name>`. That per-worktree gitdir owns HEAD but
has no `config` of its own — the shared config (including the remote
URL) lives at the target of a `commondir` pointer. The previous
`resolve_git_dir` stopped at the worktree-specific gitdir, so
`resolve_host_and_url` read nothing, `GitHost` defaulted to `Other`,
and the label rendered as `git · branch` instead of `github · branch`
for every worktree of a GitHub-hosted repo.
Split resolution into `resolve_gitdirs`, which returns a pair:
- `work_dir` — owns HEAD (worktree-specific for worktrees, identical
to `config_dir` for plain clones and submodules).
- `config_dir` — owns the remote URL (follows `commondir` when present,
handling both relative and absolute pointer forms).
`inspect` now parses HEAD from `work_dir` and the remote URL from
`config_dir`, so the host is re-classified correctly for worktrees
without perturbing the submodule path.
Covered by three new tests:
- `worktree_gitfile_resolves_to_commondir` (relative commondir)
- `worktree_commondir_with_absolute_path`
- `submodule_gitfile_still_resolves_host_end_to_end` (regression guard
for submodules — HEAD + config co-located, no commondir)
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): drop FileBrowser affordance banner
The "press [S] to use this folder" banner above the explorer is redundant
with the `S select` footer hint below it, and inconsistent with other
modal styling (no other modal has a top banner). Drop it and its layout
constraint so the explorer shifts up by one row.
The `rejected_reason` banner (shown when the operator picks $HOME itself
or a `.jackin/` path) stays — it is functional error feedback, not an
affordance hint.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): prefix git marker, "repo" -> "repository" in UI
Three small FileBrowser/git-prompt polish items:
- Prepend the git-repo marker instead of appending. Changed the glyph
from U+25A3 (white square with black small square) to U+2387
(alternative key symbol — reads as a branch) and moved it to the head
of the directory name so the eye lands on it first. The trailing
marker was easy to miss; a file listing now shows
"⎇ scentbird-root/" rather than "scentbird-root/ ▣".
Noted as a one-liner in the code: per-entry colouring would need
dropping ratatui-explorer — out of scope here.
- Rename user-facing "repo" to "repository". The button label becomes
"Mount this repository" and the prompt title becomes
"Git repository detected". Identifiers (repo_dir, GIT_REPO_MARKER,
test fixture names) are left alone — this is a UI-string change only.
- Rename the middle git-prompt button from "Enter to pick subdirectory"
to "Pick a subdirectory" — imperative voice parallel to the first
button, no more "key + verb" mix. Footer hints under the prompt
still read "E enter" for the shortcut, which remains correct.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): uppercase single-letter hotkeys in footer hints
Operator prefers the `M mount · E enter · C/Esc cancel` style
consistently across the TUI. Previously the list-view footer was
lowercase (`e edit · n new · d delete · o open in GitHub · q quit`)
while the git-prompt hint was uppercase. Normalise every footer site
on uppercase single-letter keys; multi-character glyphs (Enter, Tab,
Esc, ↑↓, etc.) and non-alpha keys (`*`) pass through unchanged.
Updates:
- `src/launch/manager/render.rs` list footer: E/N/D/O/Q
- `src/launch/manager/render.rs` editor save footer: S
- `src/launch/manager/render.rs` contextual_row_items: D/A/O on
Mounts rows, A on the "+ Add mount" sentinel
- `src/launch/widgets/file_browser.rs` nav hint: S select,
H/← up
- Existing footer test assertions updated to match new casing
- New `footer_hotkeys_are_uppercase` test scans contextual hints
(Mounts row + sentinel, Agents) and verifies every single-char
alphabetic `Key` item is uppercase
Key handlers extended to accept both cases where a footer now shows
uppercase. Most handlers already matched `'e' | 'E'` from batch 11;
the remaining lowercase-only sites (list Q/E/N/D/O, list K/J nav,
editor S/K/J, editor-Mounts A/D/O, file_browser S/H/L nav, picker
J/K) now take `'x' | 'X'`. Behavioural change is nil — Caps Lock
and Shift-held hotkeys now work where they already did at the
footer-advertised case.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): click-to-select in workspace list
Extend `handle_mouse` with row-click selection on the workspace-list
pane. Left-button Down inside the list content area maps to the row
index and updates `ms.selected`; clicks on the seam column still
start a drag (regression guard for batch 14).
Hit-test rules:
- Seam always wins — a click within ±1 column of the current seam
starts a drag regardless of y. This keeps the resize affordance
unambiguous even when the seam overlaps a valid row position.
- Otherwise, clicks inside `[1, seam - 1]` × `[header + 1, body_end - 1]`
(left-pane interior minus borders) convert to a row index via
`mouse.row - (header_height + 1)`.
- The index must be in `[0, sentinel_idx]`; beyond that we silently
drop the click. Row 0 = "Current directory", 1..=N = saved, N+1 =
"+ New workspace" sentinel.
- Clicks outside those ranges (header, footer, borders, right pane)
are ignored.
Layout heights are pulled from two new private consts mirroring
`render::render`'s `Constraint::Length(3)`/`Length(2)`. If the
chrome ever changes shape, both the render and hit-test paths need
updating together.
Double-click = launch is intentionally skipped: crossterm doesn't
emit native double-click events, so implementing it would need
tracking `(last_row, last_instant)` on `ManagerState` with a debounce
window. That's more state-machine than one item in this batch
warrants — left as a follow-up. Single-click-to-select is the
must-have and is fully wired.
Five new tests cover the happy path (row 0, mid-list row, sentinel
row), the negative path (header / borders / right pane / below
sentinel / footer), and the seam precedence regression guard.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): FileBrowser Esc steps back one level when not at root
Previously Esc always cancelled the modal. If the operator drilled into
a subfolder (via Enter on a plain dir or via the git-prompt
"Pick a subdirectory" path), a stray Esc collapsed the whole picker and
returned to the workspace list. Operators expect Esc to back out one
level — mirroring the behavior of `h` / `←` — and only cancel when
already at root.
Esc now:
- Clears any stale rejected_reason.
- Navigates one level up when cwd != root (sandbox-guarded, same as the
existing root-clamp).
- Cancels the modal only when cwd == root.
Git-prompt Esc is unchanged: it still dismisses only the prompt and
leaves the explorer open at the current cwd.
Footer hint updated from "Esc cancel" to "Esc up/cancel" (matching the
batch 16 uppercase-hotkey convention) — accurate for both drilled-in
and root cases.
Five new tests cover: esc-at-root cancels, esc-in-subfolder navigates
up, esc-three-levels-deep goes up exactly one, esc clears
rejected_reason, and the git-…
donbeave
added a commit
that referenced
this pull request
May 6, 2026
* docs(specs): workspace manager TUI (PR 2 of 3) Design spec for PR 2 of the launcher-workspace-manager series. Adds an interactive workspace manager screen to the jackin launcher — list, create, edit, and delete workspaces without dropping to CLI. Reached via `m` from the existing Workspace picker; Esc returns to the launcher. Launch path stays keystroke-identical. Key design decisions from brainstorming (all settled, no open questions): - Entry model: separate Manager screen on `m` keypress; launch path unchanged. - Editor tab set: General · Mounts · Agents · Secrets-stub. Secrets placeholder locks in the final tab strip so PR 3 fills in the panel without a visual reshuffle. - Text-edit UX: modal push — centered overlay, one reusable TextInput widget. - Staging: explicit save via `s`. Pending changes drive dirty markers; Esc with pending opens Discard/Save/Cancel. - Create flow: mounts-first wizard — file browser for host source, dst auto-defaulted to the same absolute path as src (host-path mirror), workdir picked from mount dsts + ancestors (never free-text), name last with live uniqueness check. - Delete UX: single-line Y/N confirm modal. - Style: reuses jackin's existing digital_rain (src/tui/animation.rs), step_shimmer, spin_wait, and landing-page color tokens from docs/src/components/landing/styles.css. One new area-bounded rain widget extracted from animation.rs. Three new reusable widgets emerge (TextInput, FileBrowser, Confirm) that PR 3's Secrets tab will consume unchanged. All persisted writes flow through ConfigEditor (established in PR 1, merged in #162). Non-goals: per-(workspace × agent) env overrides (PR 3), global mount management (CLI only), agent lifecycle from manager (CLI only), CLI surface changes, CHANGELOG. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(specs): lock third-party widget choices for PR 2 Amends the workspace manager TUI spec with a Third-party dependencies subsection that names the three ratatui ecosystem crates we'll adopt: - ratatui-textarea (v0.9.x) — single-line TextInput (ratatui-org owned) - ratatui-explorer (v0.3.x) — FileBrowser with folders-only wrapper - tui-widget-list (v0.15.x) — WorkdirPick list mechanics All three require the ratatui unstable-widget-ref feature flag. Rejected with rationale so reviewers don't re-litigate: tui-input (superseded by ratatui-textarea), tui-confirm-dialog / tui-overlay (Confirm modal is cheaper hand-rolled), rat-widget (too opinionated), throbber-widgets-tui / ratatui-cheese (we have spin_wait already), ratatui-toaster (banner is ~30 LOC with step_shimmer), tui-logger (jackin has no log or tracing framework today). Also updates Rollout section — "no new dependencies" was no longer accurate. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com>
donbeave
added a commit
that referenced
this pull request
May 6, 2026
* docs(plans): workspace manager TUI implementation plan
Twenty-two-task TDD-shaped implementation plan for the workspace
manager TUI specced in #164. Ordered in seven phases:
1. Foundation — deps + module scaffolds + animation.rs refactor
2. Widgets — Confirm, TextInput, FileBrowser, WorkdirPick, PanelRain
3. State machine — ManagerState, EditorState, CreatePreludeState
4. Render — list view, editor (4 tabs), modal dispatcher
5. Input — modal-first key dispatch with per-stage routing
6. Integration — LaunchStage::Manager wire-in, m keybinding, full
editor + create key handling, ConfigEditor save/create/delete paths
7. Polish — style effects (boot reveal, save shimmer, toast expire),
integration test, final verification + PR
Each task is TDD-shaped: write failing test → run fails → implement →
run passes → commit. Complete code in every implementation step. No
placeholders.
Scope cut documented in self-review: tab-slider and panel-focus-glow
animations from the spec's Style section are omitted; they're cosmetic
and can land in a follow-up PR without rework.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): scaffold workspace manager modules + widget deps
Adds three ratatui ecosystem crates (ratatui-textarea 0.9,
ratatui-explorer 0.3, tui-widget-list 0.15) and enables ratatui's
unstable-widget-ref feature. Creates empty module structures at
src/launch/widgets/ and src/launch/manager/ to land typed setters,
widgets, and state transitions in subsequent commits.
No behavior change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(tui): extract render_rain_frame for reuse
Separates the per-frame rain rendering from digital_rain's event loop
so the upcoming PanelRain widget can render bounded-area rain without
duplicating the renderer. tick_rain and RainState become pub(crate)
for the same reason. Fullscreen digital_rain is rewritten to delegate
to render_rain_frame. No visible change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Confirm widget — Y/N modal
Hand-rolled Y/N confirmation dialog. Case-insensitive, Esc cancels.
~60 LOC + 5 tests. Used by delete-workspace and discard-changes flows.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): TextInput widget — single-line via ratatui-textarea
Wraps TextArea in single-line mode (intercepts Enter and Ctrl+M so
newlines are never inserted). Exposes a ModalOutcome<String> contract:
Enter commits, Esc cancels, everything else passes through to the
textarea for cursor / insert / backspace handling.
Cursor is placed at end of initial text on construction so editing
feels natural (backspace works immediately on prefilled values).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): FileBrowser widget — wraps ratatui-explorer
Folders-only filter, seeded from $HOME by default, adds 's' as
select-current-folder. Delegates all navigation (h/l/j/k/Enter/
Backspace/Home/End/PgUp/PgDn/Ctrl+h) to ratatui-explorer defaults.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): WorkdirPick widget — choice list via tui-widget-list
Derives the pick list from mount dsts + each ancestor up to /, with
labels (mount dst / parent / root). Deduplicates when multiple mounts
share ancestors. Enter commits selected path, Esc cancels.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): PanelRain widget — area-bounded phosphor rain
Wraps tui::animation's RainState engine for rendering into a bounded
Rect. Tick + render are separate so callers control frame rate.
Resizes state when the rect changes shape.
Adds RainState::new(cols, rows) constructor to animation.rs so the
widget can initialize state without duplicating the column/grid setup
that was previously inlined in digital_rain().
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): ManagerState + EditorState + CreatePreludeState types
Defines the top-level state machine per spec § 3: ManagerStage enum
(List / Editor / CreatePrelude / ConfirmDelete), EditorState with
dirty detection and change_count, CreatePreludeState with the
mounts-first wizard step enum, Modal enum with target enums, Toast
type, and constructors. Tests cover WorkspaceSummary derivation,
ManagerState::from_config, EditorState dirty detection, and
CreatePreludeState initial step.
ManagerStage and ManagerState carry a lifetime parameter propagated
from TextInputState<'a> (ratatui-textarea borrow). MountConfig lacks
Ord/Hash so change_count uses linear containment checks rather than
BTreeSet symmetric_difference.
Transitions and key handling are filled in by subsequent tasks.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): create-workspace wizard state transitions
Mounts-first flow: PickFirstMountSrc → PickFirstMountDst → PickWorkdir →
NameWorkspace. Each accept_* method advances the step. default_mount_dst
mirrors the host src path. default_name derives from the dst basename.
build_workspace assembles the final WorkspaceConfig.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): manager list view render
Renders ManagerStage::List: header banner, horizontal-split body
(workspace list + details pane), footer hint. Other stages rendered
by subsequent tasks (12: editor, 13: modal dispatcher).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): editor view render (all four tabs)
Renders Editor stage with General / Mounts / Agents / Secrets-stub
tabs, dirty markers on changed fields, save-count footer. Error
banner overlays the top of the tab body using --landing-danger
(#ff5e7a) for real errors.
Refactors top-level render to let stages declare whether they use
shared chrome (List, future ConfirmDelete) or their own full-screen
layout (Editor, future CreatePrelude).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): modal render dispatcher
Centers a modal Rect at 60x30 percent of the frame and dispatches to
the appropriate widget's render function based on the Modal variant.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): key dispatcher with modal precedence
Scaffolds handle_key with modal-first precedence: if a modal is open
anywhere in the state machine, events route to the modal handler
before per-stage handlers. Full editor + prelude wiring lands in
Tasks 16 / 17; this commit has stubs for those to keep the compiler
happy. List and ConfirmDelete stages are fully wired (navigation,
delete flow via ConfigEditor).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): LaunchStage::Manager + m keybinding
Adds a third launch stage and wires an m keypress from the Workspace
picker to transition into it. run_launch now takes AppConfig by value
+ &JackinPaths so the manager can open ConfigEditor. Footer hint in
the Workspace stage gains 'm manage'.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): editor key handling — tabs, save, discard, field edits
Implements Tab/Shift-Tab navigation between tabs, ↑↓ row selection,
Enter-to-edit (opens modal per field type), Space/* on Agents tab,
a/d on Mounts tab. s triggers save via ConfigEditor::edit_workspace
or create_workspace, with error banner on failure. Esc with pending
changes opens the Discard confirm modal; Esc with clean state returns
to the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): full create-workspace prelude flow
Chains the four modals (file browser → dst TextInput → workdir pick →
name TextInput) through CreatePreludeState. On completion, transitions
to Editor(mode=Create) with everything pre-populated. s in the editor
creates via ConfigEditor::create_workspace.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): manager style effects
Boot reveal on manager entry via tui::animation::digital_rain(400, None).
Save toast auto-expires after 3s. Shimmer: toast text flashes white during
the first 400ms post-show. JACKIN_NO_ANIMATIONS=1 disables the rain
transition.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* test(launch): end-to-end manager delete-workspace flow
Drives manager::handle_key with scripted key events (d, y). Asserts
the workspace is removed from on-disk config, the manager transitions
back to List, and the in-memory workspace list refreshes. Regression
guard against state-machine drift.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* style(launch): quiet clippy / fmt for workspace manager
Fix all clippy warnings introduced by the workspace-manager TUI (~1500
lines of new code): unnested or-patterns, collapsible-if, elidable
lifetimes, default-trait-access, items-after-statements, match-for-
equality, match-for-single-pattern, needless-pass-by-ref-mut,
doc-markdown (missing backticks), missing-const-for-fn, uninlined-
format-args, manual-Debug-non-exhaustive, large-enum-variant (allow),
too-many-lines (allow), unnecessary-trailing-comma, and the associated
fmt diff (11 files reformatted).
No logic changes; all tests pass (workspace_config_crud requires
--test-threads=1 due to pre-existing set_current_dir race).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): replace Workspace picker with manager as initial stage
The old Workspace picker stage is removed. LaunchStage::Manager becomes
the initial stage — jackin opens directly to the manager. Enter on a
workspace launches it (via the existing Agent picker); e opens the
editor; n creates; d deletes; q/Esc exits jackin. The m keybind is gone
— nothing to enter since we are already in the manager.
Esc from the Agent picker returns to the manager list (was: Workspace
picker, which no longer exists).
Also removes the mid-loop digital_rain(400, None) boot reveal that was
fighting with skippable_sleep's raw-mode toggling, which caused arrow
keys to print as raw escape sequences in the manager instead of being
captured by crossterm events.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): render active modals in manager
render_modal was defined but never called. Modals in Editor and
CreatePrelude stages transitioned correctly in state but had no
visible effect on screen — pressing n would silently put the user in
the create wizard with an invisible FileBrowser, making the create
and edit-field flows appear broken.
Also renders the ConfirmDelete variant's confirm modal directly
(ConfirmState on ConfirmDelete is a top-level field, not wrapped in
Modal::Confirm).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): modal footer hints + stage-aware manager footer
File browser and text input modals were rendering without any hint
about the keys to commit/cancel. Users opening the create workspace
flow saw the folder picker but couldn't progress — pressing Enter
only descended into folders (s is the select key for ratatui-explorer),
and there was no visual cue about the right key.
Adds a one-line phosphor-dim italic footer inside each modal:
- FileBrowser: ↑↓ navigate · Enter open · h/← up · s select · Esc cancel
- TextInput: Enter confirm · Esc cancel
Also makes the top-level manager footer hint stage-aware:
- List stage: existing navigation hint (unchanged)
- CreatePrelude: Create workspace · follow the prompts · Esc cancel
- ConfirmDelete: Y yes · N no · Esc cancel
- Editor: still delegates to render_editor's own footer
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): Esc in create wizard returns to list immediately
Previously pressing Esc inside the first create-wizard modal cleared
the modal but left the state machine stuck in ManagerStage::CreatePrelude
with no modal active — render drew a blank body, requiring a second Esc
to reach the non-modal prelude handler that transitions back to List.
Now the post-modal check distinguishes three outcomes: in-progress
(modal still open), complete (wizard finished with name), and
cancelled (modal cleared without a name). Cancelled transitions to
List in the same input pass.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): wire rename + render all agents on Agents tab
Fixes two spec gaps:
1. Agents tab rendered only currently-allowed agents, so the user
couldn't add agents via Space toggle — non-allowed agents were
invisible. Now iterates config.agents (the full set) and shows
[x] or [ ] per agent based on pending.allowed_agents membership.
Threads &AppConfig through manager::render → render_editor →
render_agents_tab. Also fixes set_default_agent_at_cursor to use
config.agents for cursor-to-agent resolution instead of the
allowed-only list.
2. Workspace rename was a TODO. Now:
- ConfigEditor gains rename_workspace(old, new) using toml_edit's
key-rename (preserves nested tables + array-of-tables). Rejects
empty new name, collision, and missing old name.
- General tab's name row is editable on Enter (in Edit mode) via
TextInput modal.
- apply_text_input_to_pending stashes the name on
EditorState::pending_name.
- save_editor calls rename_workspace before edit_workspace when
pending_name differs, then updates editor.mode so subsequent saves
target the new name.
- change_count + is_dirty + render dirty marker all track the rename.
Tests: three new unit tests on ConfigEditor::rename_workspace covering
happy path (nested tables preserved), collision rejection, and empty-
name rejection.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): FileBrowser s commits cwd, not highlighted entry
Pressing s in an empty or file-only folder previously committed the
highlighted entry, which in such a folder is '../' — so the user got
the parent directory, not the folder they were viewing. This was
especially bad for newly-created empty workspace source folders.
Now s commits the explorer's current working directory via
FileExplorer::cwd() (ratatui-explorer 0.3.x). User intent is preserved:
'I've navigated to this folder — select it.' Footer hint updated from
's select' to 's use this folder' to reflect the semantics.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): render active-row cursor in editor tabs
EditorState::active_field tracked the cursor but render functions
didn't display it — users couldn't tell which row Enter / Space / * /
a / d would target. Add a ▸ prefix and phosphor-green bold to the
selected row across all three tabs (General, Mounts, Agents).
Also clamp Down-arrow to the last valid row so the cursor can't run
off the end of the visible content, and thread &AppConfig through to
handle_editor_key's Down handler so it can size the Agents tab's
row count correctly.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): UX polish pass on manager create/edit flows
Nine polish fixes reported after live walkthrough:
1. TextInput/Confirm modals were 30%-of-screen tall, rendered as big
empty boxes around a single line. Now variant-aware: inputs/confirms
are 5-6 rows fixed; file browser and workdir pick stay taller for
their scrolling lists.
2. 'last used' row hidden in Create mode (no history exists).
3. 'default agent' row hidden in Create mode (no agents picked yet).
4. Footer hint is now row-contextual: 'Enter rename' on name row,
'Enter pick workdir' on workdir row, 'a add / d remove' on mounts,
'Space toggle / * set default' on agents, nothing on read-only
rows. Base hint says 's save workspace' (was 's save') for clarity.
5. File browser gets a prominent outer block titled '<cwd> · press
[S] to use this folder' — the select affordance was previously
buried in a dim footer line.
6. Mount rows collapse 'src → dst' to just 'path' when src == dst
(host-path-mirror default — redundant arrow gone).
7. Mounts tab '+ Add / − Remove selected' footer uses white-bold for
the action words to distinguish from the mount list.
8. Agents tab gets a top banner clarifying empty = 'all allowed'
semantics vs non-empty = custom allow-list.
9. Read-only rows (last used) no longer advertise Enter in the footer.
Also fix max_row_for_tab in input.rs: Create mode General tab only
has 2 rows (name read-only + workdir), not 4.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): Confirm modal shows Y/N as styled buttons
Previously the modal rendered '[Y]es · [N]o (default) · Esc cancel' as
inline text inside a tall 30%-of-screen box, which looked like a
multi-line textarea. Now:
- Modal is compact (6 rows)
- Yes/No render as inverted-video buttons, centered
- No (default) uses white-on-black to distinguish as default action
- Esc cancel moves to a dim italic footer hint at the bottom
- Prompt text stays bold-white at the top
Enter intentionally unbound — destructive confirms should not commit
on accidental Enter presses.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): remove nested borders in FileBrowser modal
ratatui-explorer's widget renders its own bordered block with the CWD
as title. The prior polish pass added an outer block with 'press [S]
to use this folder' — the result was double borders, ugly and
confusing.
Drop the outer block. Show the 'press [S]' affordance as a bold-white
centered line ABOVE the explorer (no border), and keep the dim
navigation hint as a line BELOW. The explorer's own cwd-titled block
stays — no nesting.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Confirm modal gains Tab focus + Enter commits focused
Adds standard confirmation-dialog UX: Tab / Shift+Tab / ←→ / h/l cycle
focus between Yes and No; Enter commits the focused button. Default
focus is No (destructive action protection — accidental Enter won't
commit Yes). Y/N direct shortcuts still work regardless of focus.
Visual: focused button gets white bg + black text + bold; unfocused
gets phosphor-green bg. Footer hint updated to mention Tab + Enter.
Modal grows from 6 to 7 rows for a second spacer between buttons
and hint (was visually cramped).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): shrink FileBrowser + WorkdirPick modal heights
Prior sizing was 60 rows for FileBrowser and 40 rows for WorkdirPick —
effectively fullscreen on a typical 40-50 row terminal. Tight 20 and
12 rows fit comfortably and still show enough entries without the
modal swallowing the whole screen.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): + Add mount is a selectable sentinel row
Mirrors the + New workspace sentinel in the manager list. The Mounts
tab now renders + Add mount as a real selectable row at the end of
the list, selected via ↑↓, activated via Enter. Visual treatment is
white bold (distinguishing it from the green mount rows).
- max_row_for_tab reports len() (mount count + sentinel index) for
Mounts so ↓ can reach the sentinel.
- remove_mount_at_cursor is a no-op on the sentinel (guard already existed).
- a (anywhere on the tab) still works as a quick-add shortcut.
- Contextual footer hint differentiates between 'on a mount row'
(d remove · a add) and 'on the sentinel' (Enter add · a add).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Agents tab shows [all] / [custom] status badge
Replaces the implicit 'empty list = all allowed' with an explicit
status line at the top of the Agents tab:
Allowed agents: [ all ] (when allowed_agents is empty)
Allowed agents: [ custom ] (3 of 5 allowed) (when non-empty)
The badge is an inverted-video token (phosphor-green bg for 'all',
white bg for 'custom') making the current mode immediately visible.
The agent list below stays as a checklist — toggling updates the
status badge live.
Cursor semantics also shift: cursor is now 0-based into config.agents
(no more header-offset-by-one). toggle_agent_allowed_at_cursor and
set_default_agent_at_cursor are updated accordingly. max_row_for_tab's
Agents arm drops to len()-1.
set_default_agent_at_cursor now also auto-allows the agent being set
as default (was previously a no-op if the agent wasn't already in
allowed_agents).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Mounts tab shows folder / git · <branch>
Adds a mount_info helper that inspects the host-side src path on
render: checks for .git as dir or submodule-gitfile, reads HEAD, and
reports the current branch (or detached short-sha). Renders next to
each mount row as dim italic metadata:
/Users/…/repo (rw) · git · main
/Users/…/scratch (rw) · folder
/Users/…/gone (rw) · missing
Six unit tests cover: missing path, plain folder, normal repo with
branch, detached HEAD, submodule .git file, label formatting.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): Save/Discard/Cancel modal + richer details pane
Two UX upgrades:
1. Exit-with-changes now offers three explicit choices instead of
binary 'Discard Y/N'. New SaveDiscardCancel modal with three
buttons (Save / Discard / Cancel), Tab cycles focus, Enter commits
the focused option. S/D/C/Esc shortcuts work regardless of focus.
Default focus is Cancel (safest). Save intent triggers ConfigEditor
save → list; Discard just drops pending; Cancel keeps the editor.
2. Manager list's details pane now shows the full mount list (with
folder / git · <branch> labels, same as the Mounts tab) and the
allowed-agents list (or 'any agent' when unrestricted). Title drops
the duplicate workspace name since the list selection already shows
it.
5 new unit tests on SaveDiscardState covering focus cycling and key
shortcuts.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): restrict FileBrowser to \$HOME, rename main title
Four UX fixes:
1. Main manager screen title 'manage workspaces' → 'workspaces'
(the screen does more than manage — launch, create, edit, delete).
2. FileBrowser modal goes fullscreen (100% x 100%) so the main chrome
doesn't peek through and confuse the visual.
3. FileBrowser now:
- Starts at \$HOME (already did)
- Excludes Library, Applications, Movies, Music, OrbStack, Pictures
from the listing via filter_map
- Clamps cwd back to \$HOME if the user escapes above it via
set_cwd() (ratatui-explorer 0.3.x has this method)
- Rejects \$HOME itself as a workspace source
- Rejects ~/.jackin/* (jackin's reserved data area)
4. Rejected selections show an inline red error banner
(#ff5e7a) above the explorer. Cleared on next keypress.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): display paths as ~/… via shorten_home
Paths starting with $HOME now render as '~/...' in the TUI:
General tab workdir, Mounts tab rows, details pane mounts/workdir,
WorkdirPick choices. Consistent with jackin's existing shorten_home
helper (already used elsewhere in the launcher).
Paths stored on disk are unchanged — this is display-only.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): mount table formatting + FileBrowser resize/colors
Two fixes:
1. Mount lists in the details pane and Mounts tab render as an
aligned 3-column table (path, mode, type) instead of a free-form
line where the '(rw)' tag and type metadata floated at variable
positions. shorten_home applied to paths consistently via the
shared format_mount_rows helper, which is called from both
render_details_pane and render_mounts_tab.
2. FileBrowser modal goes from fullscreen (100%) to 70%x70%, letting
the surrounding chrome show again so the dialog reads like a
dialog, not a whole screen. Theme configured to use jackin's
phosphor palette (green text, bright-phosphor highlight, shortened
CWD title via shorten_home in a dynamic with_title_top closure).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): drop Agents 'default' column + cap workspace list height
1. Agents tab header 'allowed? · default · agent' → 'allowed? · agent'.
The star marker next to the agent name already indicates default;
the dedicated column was empty for every non-default row.
2. Manager list body now caps at content height (workspace count + 2
border rows + 1 sentinel row) instead of filling the whole frame.
5-6 workspaces no longer render in a box that looks two-thirds
empty.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* Revert "fix(launch): cap workspace list height to content"
The height cap made the space below the boxes visibly empty, which
reads worse than the previous full-height boxes. User feedback:
'before it was better when it was using the whole vertical space.'
Keeps the Agents tab header change from the same original commit
(3fdab9f3) — only the list-body sizing is reverted.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): hide FileBrowser .. entry at $HOME root
Previously the '../' entry was always shown in the file browser.
When the user was at $HOME, selecting it would escape the sandbox
(and was then clamped back by set_cwd) — confusing and cluttered.
Now the filter hides '..' when its target path is outside the root
subtree. At $HOME the entry disappears; at any subfolder of $HOME
it still appears so the user can navigate back up.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): hide empty right pane, split details, clickable git links
Three UX improvements:
1. When the cursor is on '+ New workspace' in the manager list,
the right details pane is hidden entirely — the list takes full
width. No more empty bordered box for the sentinel row.
2. Details pane split into three stacked sub-panels: General (workdir
+ last used), Mounts (tabular with header row), Agents (list or
'any agent'). Each has its own bordered mini-block with phosphor-
dark border and white-bold title. The outer 'Details' block is gone.
3. Git branch URL resolution wired up: inspect() now parses
<git_dir>/config to find the origin remote and derives a web URL
(GitHub, GitLab, generic HTTPS/SSH). MountKind::Git gains a
web_url: Option<String> field; MountKind::labeled_hyperlink() wraps
the branch name in OSC 8 escape sequences for supported terminals
(iTerm2, kitty, WezTerm, Alacritty, modern Terminal.app).
OSC 8 fallback: ratatui's Paragraph widget strips raw ESC bytes, so
the render path continues to call label() (plain text). The
hyperlink infrastructure (labeled_hyperlink, osc8_link, web_url) is
retained for a future raw-terminal-write path. Both are annotated
#[allow(dead_code)] with an explanatory TODO.
5 new unit tests on remote-URL parsing (GitHub SSH, GitHub HTTPS,
ssh:// protocol, GitLab SSH, config-file parse). All 566 tests pass.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): polish FileBrowser, WorkdirPick, and mount-dst prompt
- FileBrowser entries now render white instead of phosphor-green so the
bright-green highlight is the unambiguous focus indicator.
- TextInput prompts for mount destination say "destination (default:
same as host path)" instead of the internal "Mount dst" phrasing.
- WorkdirPick lines are laid out as a table: the path column is padded
to the widest choice so the dim+italic label column (`(mount dst)`,
`(parent)`, `(root)`, `(home)`) lines up cleanly.
- WorkdirPick filters `/` and the literal parent of `$HOME` (e.g.
`/Users` on macOS, `/home` on Linux) from the choice list — those
paths are never useful workdir targets.
- When a path is exactly `$HOME`, label it `(home)` instead of
`(parent)` so the workspace operator sees a recognisable name.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): FileBrowser s commits highlighted folder
Previously `s` always committed the explorer's cwd, which meant the
operator had to press Enter to navigate into the target folder before
committing — even though the folder was already highlighted and the
target of a single Enter press.
Reading `FileExplorer::current()` lets us commit the highlighted entry
directly when it is a real child directory. The synthetic `../`
parent-link row and the empty-listing case both fall back to the cwd,
preserving the previous behaviour for those edge cases.
The existing $HOME and `~/.jackin/*` rejection rules apply to whichever
path is chosen as the commit target.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): keep General-tab labels white; note agent-hyperlink TODO
- render_editor_row and render_editor_readonly_row no longer shift the
label column to phosphor-green when the row is focused. Labels stay
white (bold when focused); values keep their phosphor colouring for
editable rows and dim phosphor for read-only rows.
- Read-only rows used to render everything in phosphor-dim, which made
the editor view look washed-out. They now match the editable-row
label treatment (white) with a dim value + italic "(read-only)"
suffix, giving the operator a cleaner signal-to-noise ratio.
- Added a TODO in render_agents_subpanel mirroring the existing
labeled_hyperlink() note in render_mounts_subpanel: ratatui's
Paragraph strips OSC 8 ESC sequences, so agent-name → GitHub links
stay plain-text until a raw-write path exists.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): gate editor save on mount-collapse plan
The editor used to write configs straight to disk via
`ConfigEditor::edit_workspace` (or `create_workspace`), which meant the
operator could save a workspace with overlapping mounts like
`~/Projects` and `~/Projects/test`. The CLI rejects this unless you
confirm or pass `--prune`; the TUI now does the same.
Flow:
- On `s`, run `workspace::planner::plan_edit` (Edit) or `plan_create`
(Create) against the pending mount set.
- `CollapseError::{ReadonlyMismatch, ChildUnderExistingParent}` ->
error banner, no write.
- Pre-existing collapses only (no edit-driven) -> error banner
referencing `jackin workspace prune <name>`. The operator can't fix
these from the editor alone and the CLI prune command already exists
for this case.
- Edit-driven collapses -> open a `Modal::Confirm` with a
`ConfirmTarget::SaveCollapse` target, listing each child/parent pair
in the same wording as the CLI. On Yes, the save re-enters with
`EditorState::collapse_approved = true` and commits the collapsed
mount set via `plan.effective_removals` / `plan.final_mounts`. On No
/ Esc, pending mounts are kept intact so the operator can edit by
hand.
Pattern: a boolean flag on `EditorState` + a new `ExitIntent::RetrySave`
variant so the confirm-yes path reuses the existing modal-exit routing
but stays in the editor on success (rather than bouncing to the
workspace list, which is what `ExitIntent::Save` does). The plan
itself is not stashed; it is cheap to recompute on re-entry.
The `Confirm` widget now grows its prompt region to match the number
of lines in `state.prompt`, and `render_modal` sizes the outer rect
via `confirm::required_height` so multi-line collapse summaries render
without clipping.
Tests (5 new):
- `save_editor_opens_confirm_on_edit_driven_collapse`
- `confirming_collapse_writes_collapsed_set`
- `cancelling_collapse_keeps_pending_mounts_intact`
- `readonly_mismatch_produces_error_banner_no_write`
- `pre_existing_collapse_produces_prune_error_banner`
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): structured footer with per-item styling
Introduce a `FooterItem` enum (Key / Text / Dyn / Sep / GroupSep) and a
shared `render_footer` that emits spans with a consistent palette:
- Key glyphs (↑↓, Enter, e/n/d/q, Tab, Esc, S, Y/N, *, Space) render in
WHITE + BOLD so they pop out of the legend.
- Action labels ("launch", "edit", "new", …) render in PHOSPHOR_GREEN.
- Inline dots (·) render in PHOSPHOR_DARK as a faint separator.
- A GroupSep (three spaces, no style) introduces a wider visual gap
between logical groups — navigation, per-row actions, and exit.
Migrate every footer call site to this scheme:
- `manager/render.rs` List / CreatePrelude / ConfirmDelete / Editor
footers build `Vec<FooterItem>` explicitly so the grouping is
deliberate per stage.
- Agent-screen footer in `launch/render.rs` uses the same inline spans.
- Modal-local hints inherit the scheme (file_browser navigation + "[S]
to use this folder" affordance, text_input "Enter confirm · Esc
cancel", confirm "Tab cycle · Enter confirm · Y yes · N no", and
save_discard).
Add unit tests covering the span-style mapping per variant plus
smoke tests for the List and ConfirmDelete stage footers.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): keep right pane visible on '+ New workspace' sentinel
Batch 7 expanded the list to full width when the cursor landed on the
sentinel row. The operator wants the 45/55 split preserved — the layout
should not shift as the cursor moves — with the right pane rendered as
an empty bordered block (same PHOSPHOR_DARK border as the General /
Mounts / Agents sub-panels) when there is no workspace to describe.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align mount-table header with data columns
The mount-table header was a hardcoded string (" path<23 spaces>mode<3
spaces>type") while data rows computed their path column width
dynamically from the widest row. When paths were shorter than 23 chars
the header appeared drifted relative to the data; when they were longer
the header's "mode" column collided with the data's mode column at a
different offset.
Share the column-width computation between the header and data rows:
- Extract `mount_path_width` which returns max(row_path, "path".len(),
10) so the header and data always use the same column boundary.
- Add `render_mount_header(path_w)` that uses the same format string as
the data rows, then have both the read-only details subpanel and the
editor Mounts tab consume it.
- Pin the `mode` column to a shared `MOUNT_MODE_COL_WIDTH = 4` constant
(covering "mode" as well as "rw"/"ro" + trailing space) so it no
longer over-pads inconsistently.
Add unit tests that build mount rows with mixed path lengths and assert
the header's "mode" column starts at the same character index as each
data row's "mode" column.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): describe workspace concept on '+ New workspace' pane
Replace the empty bordered block shown to the right of the manager list
when the sentinel row is focused with a two-panel description pulled
from the "What is a workspace?" / "Why save a workspace?" sections of
the workspaces guide. Keeps the right-hand real estate useful for
first-time operators and matches the General/Mounts/Agents sub-panel
chrome for visual consistency.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): restore 'Current directory' row in workspace manager
Before the TUI redesign the launcher's first row was a synthetic
"Current directory" choice that let operators launch an agent against
cwd without saving a workspace. The manager's rewrite dropped it; this
reinstates it as row 0 of the list with the right-pane summary, the
cwd-aware preselect, and the launch wiring that matches the old
behaviour.
Row layout (enforced by ManagerState::from_config, render_list_body,
and handle_list_key):
row 0 → synthetic "Current directory"
rows 1..=N → saved workspaces
row N+1 → "+ New workspace" sentinel
Edit (`e`) and Delete (`d`) are rejected on row 0 with a toast. Enter
on row 0 emits a new InputOutcome::LaunchCurrentDir; the run-loop
routes it through the same agent-picker transition as LaunchNamed,
reusing LaunchState::workspaces[0] (the CurrentDir choice built by
LaunchState::new). Preselect reuses find_saved_workspace_for_cwd so
TUI and CLI agree on "which workspace am I in?".
The right pane branches on row 0 → render_current_dir_details_pane
(dedicated renderer; no last-used row, no edit affordance, "any
agent"). The sentinel description pane lands in the same commit's
sibling already; saved-workspace rows continue to use the shared
render_details_pane with `workspaces[selected - 1]`.
Tests added:
- manager_preselects_saved_workspace_matching_cwd
- manager_preselects_current_directory_when_no_saved_matches
- manager_current_directory_is_first_row
- current_directory_row_rejects_edit_and_delete
- enter_on_current_directory_returns_launch_current_dir
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): polish mount-header gap, modal titles, and FileBrowser size
- Mount table header: add two-space gutter between `mode` and `type`
so the header no longer reads "modetype". Data rows now emit the
matching two-space gap so the `type` column aligns in both the
read-only Mounts subpanel and the editor Mounts tab.
- Text-input + Workdir-pick modal block titles render WHITE + BOLD to
match the General/Mounts/Agents block titles on the main screen.
Confirm + SaveDiscard already use WHITE+BOLD — left untouched.
- WorkdirPick path values render WHITE (the `(mount dst)`/`(parent)`/
`(home)`/`(root)` label suffix stays PHOSPHOR_DIM italic).
- FileBrowser modal height drops from 70 absolute rows to 22 so it
no longer eats the whole screen. Width stays at 70%.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): classify git mounts by host and relabel GitHub remotes
- Introduce `GitHost { Github, Other }` on `MountKind::Git` so the
render path can tell which remotes have an "open in browser"
affordance. `inspect` populates this from `parse_remote_origin_url`:
SSH `git@github.com:`, HTTPS `https://github.com/…`, and
`ssh://git@github.com/…` all resolve to `Github`; anything else
(self-hosted, GitLab, no remote, unparseable URL) falls through to
`Other`.
- `remote_to_web` now returns `Some(url)` only for GitHub hosts and
`None` for everything else — it no longer synthesises `gitlab.com`
URLs. Non-GitHub remotes keep `web_url: None` on the `MountKind`.
- `MountKind::label()` renders `github · {b}` / `github · detached {sha}`
/ `github` for GitHub hosts and keeps the generic `git · …` prefix
for `Other`. `MountKind::Folder` / `Missing` unchanged.
- `remote_to_web_gitlab` test re-purposed to assert GitLab (and other
non-GitHub hosts) now return `None`. New tests for the GitHost split
via `inspect` and for the `remote_points_at_github` predicate covering
all three URL forms + a GitHub-lookalike subdomain rejection.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): 'o' key opens highlighted GitHub mount in the browser
- Add the `open` crate (5.x) so the editor can launch the system
browser without blocking the TUI (`open::that_detached`).
- Wire `o` into the editor's Mounts tab: when the cursor is on a
mount row whose source resolves to a GitHub-hosted repo with a
web URL, pressing `o` opens that URL in the operator's default
browser. Non-GitHub / folder / missing mounts emit an "no GitHub
URL for this mount" toast so the hint is discoverable; the sentinel
"+ Add mount" row is a silent no-op.
- `contextual_row_items` now composes an `o open in GitHub` item
onto the existing `d remove · a add` pair when the current row is
a GitHub mount. List-view mounts pane is unchanged — the `o` key
only binds in the editor.
- Tests: `github_mount_row_includes_open_in_github_hint` and
`non_github_mount_row_omits_open_in_github_hint` pin the footer
composition. No unit test for the browser side-effect itself.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): flag git repos in FileBrowser and offer mount-or-dive prompt
Part A — directory listing:
- `annotate_file` (new filter_map body) stats each directory for a
`.git` child and appends U+25A3 (▣) to the display name when present.
Works for plain clones (`.git` is a directory) and submodules (`.git`
is a file containing `gitdir: …`). Single stat per entry — no
recursive walk.
Part B — Enter on a git-repo row:
- New `GitPromptFocus { MountHere, EnterIn, Cancel }` + two fields on
`FileBrowserState` (`pending_git_prompt`, `pending_git_focus`) drive
an in-widget confirm overlay. Enter on a git-repo row opens the
prompt; Tab/←→/h/l cycle focus; Enter commits the focused option;
M/E/C are direct shortcuts; Esc dismisses the prompt without
cancelling the browser.
- MountHere commits the repo path through the same sandbox rules as
`s` (rejects root / `~/.jackin/*`). EnterIn navigates into the repo
via `explorer.set_cwd` (avoids re-posting Enter, which would re-open
the prompt). Cancel just clears state. Non-git folders keep their
usual Enter-navigates-in behavior.
- Overlay renders as a centred 3-button bar inside the explorer area
so the listing stays visible as context. Phosphor palette + focus
styling mirrors `confirm.rs`/`save_discard.rs`; button ring copied
locally rather than cross-importing between widgets.
- Footer legend swaps to `Tab cycle · Enter confirm · Esc cancel`
while the prompt is active.
Tests cover: marker on `.git`-dir and submodule-`.git`-file cases, no
marker on plain folder, Enter opens prompt on repo row, MountHere
commits the path, EnterIn navigates in and clears prompt, Cancel
clears without cwd change, Esc dismisses prompt without cancelling
browser, plain-folder Enter navigates as before, and the `M` shortcut
commits regardless of current focus.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): add mount-destination choice modal widget
Introduces the `mount_dst_choice` widget and wires a new
`Modal::MountDstChoice` variant into the manager's modal enum plus
render dispatcher. No input behaviour changes yet — follow-up commits
swap the Editor and Prelude FileBrowser→TextInput chains to route
through this modal.
The widget is the 3-button focus-ring pattern pioneered by
`save_discard`: default focus on `OK`, Tab/BackTab cycling, and single-
letter shortcuts (`o`/`e`/`c`). Default on `OK` because the common case
is `dst = src`, so an accidental Enter commits that without surprise.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): route editor add-mount through destination choice modal
The FileBrowser→TextInput chain in the Editor's Mounts tab assumed
every operator wanted to edit the destination path. In practice, 95%
of mounts commit with dst = src. Swapping in the new MountDstChoice
modal makes the common path a single Enter press and keeps the old
behaviour one keystroke away via `Edit destination`.
`apply_file_browser_to_editor` now opens MountDstChoice instead of
pushing a provisional mount plus TextInput. The actual push happens
in the MountDstChoice commit handler:
- OK: push MountConfig { src, dst = src, rw }, close modal.
- Edit destination: push the provisional mount (as today) and open
Modal::TextInput{MountDst} pre-filled with src. The existing
TextInputTarget::MountDst handler overwrites the provisional dst.
- Cancel / Esc: close the modal, leave pending.mounts untouched.
Behavioral tests pin all three paths and guarantee no mount is
pushed until the operator commits in the choice modal.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): route create-prelude add-mount through destination choice
Mirrors the editor-side change: the Create wizard's FileBrowser step no
longer assumes the operator wants to edit the destination. Instead, the
prelude now opens MountDstChoice after FileBrowser commits, offering
the fast `OK` path that skips TextInput entirely.
Both paths (OK and TextInputDst commit) share a new helper
`prelude_advance_to_workdir_pick` so the downstream WorkdirPick stage
receives the same staged mount regardless of whether the operator
edited the destination. This keeps the chain FileBrowser → (choice) →
WorkdirPick → TextInput(Name) intact for the `OK` shortcut.
Cancel on MountDstChoice matches today's Esc-during-TextInput
behaviour: close the modal, leave the prelude state alone so the
outer dispatcher treats it as a wizard-cancellation and returns to
the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): extract mount-dst-choice dispatch to keep clippy happy
The inline `Modal::MountDstChoice` arm inside `handle_editor_modal`
pushed the function above clippy's 100-line ceiling. Extract the
outcome dispatch into `dispatch_editor_mount_dst_choice` and tidy the
helper's doc comment so `TextInput` doesn't trip the missing-backticks
lint. No behavioural change.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): strip trailing slash in FileBrowser name filter
ratatui-explorer appends `/` to directory names at runtime, so the
filter in `annotate_file` was comparing `"Library"` against `"Library/"`
and silently letting every excluded entry render. Same bug let `..`
through on the sandbox-escape check. Normalize with
`trim_end_matches('/')` before matching, and harden the `s`/Enter
paths + default key dispatch to guard against empty listings (the
fixed filter can now produce an empty `files()` which made
`current()` and nav-key dispatch panic inside ratatui-explorer).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* polish(launch): simplify Destination modal title
Rename the mount-destination TextInput label from
`destination (default: same as host path)` to plain `Destination`.
The parenthetical hint is redundant after batch 12: the TextInput
only opens when the operator explicitly picks "Edit destination"
on the MountDstChoice modal, so they're already in deliberate-edit
mode with the src pre-filled as the default.
Also capitalizes the title to match the other modal block titles
(Confirm, Unsaved changes, Mount destination, Git repo detected,
Workdir pick, Rename workspace, Name this workspace). No other
titles needed changes — the audit was clean.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): bind Left/Right to prev/next tab in Editor
Extend `handle_editor_key` so Right matches Tab (forward cycle) and
Left matches BackTab (reverse cycle). Wrap-around behavior mirrors
the existing Tab contract: General → Mounts → Agents → Secrets →
General, and symmetrically for reverse.
Modal-open precedence is already guarded by the early-return in
`handle_key` — Left/Right continue to feed into modal handlers
(Confirm, SaveDiscard, MountDstChoice) when a modal is active.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): wire list-view `o` to open workspace GitHub mounts
Extend the `o` key beyond the Editor's Mounts tab to the workspace
list view. On a saved workspace row:
0 GitHub mounts → toast "no GitHub URLs for this workspace"
1 GitHub mount → open::that_detached immediately
≥2 GitHub mounts → open a new GithubPicker modal; Enter commits
the highlighted URL to open::that_detached.
Row 0 (Current directory) and the `+ New workspace` sentinel toast
`no workspace selected` for discoverability. The list footer now
surfaces `o open in GitHub` only on rows whose workspace resolves
to ≥1 GitHub-hosted mount.
Adds:
- new `widgets/github_picker.rs` widget (title-styling and tab-list
pattern mirror WorkdirPick so the modal feels native);
- `Modal::GithubPicker { state }` variant, with arms closed in the
render-size switch, `handle_editor_modal` (defensive cancel), and
a new `handle_list_modal` dispatcher;
- `list_modal: Option<Modal<'a>>` slot on ManagerState — list-view
modals weren't previously anchored anywhere; Editor/CreatePrelude
keep their per-stage modal slots unchanged;
- `resolve_github_mounts_for_workspace` helper, shared by the input
handler and the render-side footer-hint guard.
Piggybacks a one-line clippy fix in file_browser.rs
(`iter().any(|x| *x == bare)` → `EXCLUDED.contains(&bare)`) that
surfaced after the trailing-slash filter landed.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): drop cwd suffix from Current directory row label
The list row for the synthetic "Current directory" choice used to read
`Current directory (~/Projects/foo)`. The right-pane details already
show the cwd on the `workdir` line, so the parenthetical suffix is
duplicate visual load. Render just `Current directory`.
Row 0 keeps its WHITE colour so the synthetic choice still visually
separates from the phosphor-green saved workspaces below it.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align mount-table type column with header
MOUNT_MODE_COL_WIDTH was 2, matching the literal width of rw/ro but
leaving a 2-char gap before the data row's kind column versus the
header's 4-char "mode" label. Header and data rows shared the same
"{mode:<mw} type" format string but MOUNT_MODE_COL_WIDTH no longer
matched the header label length, so `type` and its data (e.g. "folder")
rendered at different offsets.
Pin MOUNT_MODE_COL_WIDTH to 4 so rw/ro pad to the header's "mode"
width. Both the header and data emit the same two-space gutter before
the `type` column, so the kind label lines up with the header offset.
Extend the existing gap-between-mode-and-type test to additionally
assert that `header.find("type") == data.find("folder")` — the
type-column offset must match for a row with a plain folder mount.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): drop dead OSC 8 hyperlink scaffolding
The `o`-opens-in-system-browser path (via open::that_detached)
supplanted the aspirational OSC 8 hyperlinks-in-terminal route. The
OSC 8 helpers were already #[allow(dead_code)] — remove them:
- `osc8_link()` — wrapped text in OSC 8 ESC sequences;
- `MountKind::labeled_hyperlink()` — built a GitHub-linked label
from a branch/sha + url;
- their associated NOTE blocks on render.rs (mounts subpanel) and
the agents-hyperlink TODO next to render_agents_subpanel.
The `web_url: Option<String>` field stays — the `o` key consumes
it to open the branch URL. Likewise `remote_to_web`,
`parse_remote_origin_url`, and the `GitHost::Github` classification
are all still in the live path.
Clippy baseline (4) unchanged; no tests touched.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): mouse-draggable list/details divider
Add a draggable seam between the workspace list pane and the details
pane in the manager TUI. Click-and-drag on the seam column (within ±1)
resizes the split; the percentage is clamped to [20, 80] so neither
pane can be starved.
Mechanically:
- ManagerState gains `list_split_pct: u16` (default 45) and
`drag_state: Option<DragState>`. `clamp_split` + split-range consts
live alongside. `render_list_body` reads `list_split_pct` instead
of the hard-coded 45/55.
- `src/launch/mod.rs` enables `EnableMouseCapture` after entering the
alternate screen and `DisableMouseCapture` in the terminal guard's
Drop. Side-effect: the terminal's native click-drag text selection
stops working while the TUI is running — hold Shift (Terminal.app,
iTerm2) or Option (iTerm2) to bypass. Documented inline.
- The run-loop now matches on `Event::{Key, Mouse, _}` (was a bare
`if let Event::Key`). Mouse events in the Manager stage dispatch
to a new `manager::input::handle_mouse` with the current terminal
size as a `ratatui::layout::Rect`.
- `handle_mouse` hit-tests the seam, captures a `DragState` anchor
on `Down(Left)`, updates `list_split_pct` on `Drag(Left)`, and
clears the anchor on `Up(Left)`. It also gates on List stage, no
open list-modal, and `term_size.width >= 40`.
Unit tests (8 new, pure state manipulation — no ratatui loop):
- mouse_down_on_seam_starts_drag
- mouse_drag_updates_split_pct
- mouse_drag_clamps_to_min_and_max
- mouse_up_ends_drag
- mouse_down_far_from_seam_does_not_start_drag
- drag_ignored_when_list_modal_open
- drag_ignored_on_non_list_stage
- drag_ignored_when_terminal_too_narrow
Clippy baseline (4) unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): rename current-dir pane first block to "General"
The synthetic "Current directory" row has a right-pane first block titled
" Current directory ", but the left-list row label already conveys that
context. Rename to " General " to match the saved-workspace details pane
(General / Mounts / Agents) so both panes use the same three sub-panel
titles.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): remove phantom empty row in current-dir Mounts block
`render_current_dir_details_pane` hard-coded the Mounts block at
`Constraint::Length(5)`, which over-allocated by one row for the
single-mount current-directory case and left a visible empty line
inside the block border.
Extract the height formula from `render_details_pane` into a shared
`mount_block_height` helper (2 borders + 1 header + max(1, N) data rows,
clamped to 12) and use it from both pane renderers so the two paths
produce identically-tight Mounts blocks.
Covered by four regression tests pinning the formula for the empty,
single, multi, and many-mount cases.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): align General sub-panel content with Mounts/Agents
The General sub-panel (on both the saved-workspace pane and the
current-directory pane) rendered its `workdir`/`last` rows flush against
the block's left border, while the Mounts and Agents sub-panels already
used a two-space indent. The mismatch gave the right pane a jagged left
edge across the three stacked blocks.
Add the same two-space prefix to the General rows on both panes. The
convention is pinned by a new `SUBPANEL_CONTENT_INDENT` constant and
two visual regression tests that render each sub-panel to a
`TestBackend` buffer and assert the first visible character of row 0
sits at that indent relative to the block's left border. Covers the
"any agent" fallback and the starred-default-agent row explicitly.
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): follow worktree commondir for GitHost detection
Git worktrees are checkouts whose `.git` is a file pointing at
`<main>/.git/worktrees/<name>`. That per-worktree gitdir owns HEAD but
has no `config` of its own — the shared config (including the remote
URL) lives at the target of a `commondir` pointer. The previous
`resolve_git_dir` stopped at the worktree-specific gitdir, so
`resolve_host_and_url` read nothing, `GitHost` defaulted to `Other`,
and the label rendered as `git · branch` instead of `github · branch`
for every worktree of a GitHub-hosted repo.
Split resolution into `resolve_gitdirs`, which returns a pair:
- `work_dir` — owns HEAD (worktree-specific for worktrees, identical
to `config_dir` for plain clones and submodules).
- `config_dir` — owns the remote URL (follows `commondir` when present,
handling both relative and absolute pointer forms).
`inspect` now parses HEAD from `work_dir` and the remote URL from
`config_dir`, so the host is re-classified correctly for worktrees
without perturbing the submodule path.
Covered by three new tests:
- `worktree_gitfile_resolves_to_commondir` (relative commondir)
- `worktree_commondir_with_absolute_path`
- `submodule_gitfile_still_resolves_host_end_to_end` (regression guard
for submodules — HEAD + config co-located, no commondir)
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): drop FileBrowser affordance banner
The "press [S] to use this folder" banner above the explorer is redundant
with the `S select` footer hint below it, and inconsistent with other
modal styling (no other modal has a top banner). Drop it and its layout
constraint so the explorer shifts up by one row.
The `rejected_reason` banner (shown when the operator picks $HOME itself
or a `.jackin/` path) stays — it is functional error feedback, not an
affordance hint.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): prefix git marker, "repo" -> "repository" in UI
Three small FileBrowser/git-prompt polish items:
- Prepend the git-repo marker instead of appending. Changed the glyph
from U+25A3 (white square with black small square) to U+2387
(alternative key symbol — reads as a branch) and moved it to the head
of the directory name so the eye lands on it first. The trailing
marker was easy to miss; a file listing now shows
"⎇ scentbird-root/" rather than "scentbird-root/ ▣".
Noted as a one-liner in the code: per-entry colouring would need
dropping ratatui-explorer — out of scope here.
- Rename user-facing "repo" to "repository". The button label becomes
"Mount this repository" and the prompt title becomes
"Git repository detected". Identifiers (repo_dir, GIT_REPO_MARKER,
test fixture names) are left alone — this is a UI-string change only.
- Rename the middle git-prompt button from "Enter to pick subdirectory"
to "Pick a subdirectory" — imperative voice parallel to the first
button, no more "key + verb" mix. Footer hints under the prompt
still read "E enter" for the shortcut, which remains correct.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* refactor(launch): uppercase single-letter hotkeys in footer hints
Operator prefers the `M mount · E enter · C/Esc cancel` style
consistently across the TUI. Previously the list-view footer was
lowercase (`e edit · n new · d delete · o open in GitHub · q quit`)
while the git-prompt hint was uppercase. Normalise every footer site
on uppercase single-letter keys; multi-character glyphs (Enter, Tab,
Esc, ↑↓, etc.) and non-alpha keys (`*`) pass through unchanged.
Updates:
- `src/launch/manager/render.rs` list footer: E/N/D/O/Q
- `src/launch/manager/render.rs` editor save footer: S
- `src/launch/manager/render.rs` contextual_row_items: D/A/O on
Mounts rows, A on the "+ Add mount" sentinel
- `src/launch/widgets/file_browser.rs` nav hint: S select,
H/← up
- Existing footer test assertions updated to match new casing
- New `footer_hotkeys_are_uppercase` test scans contextual hints
(Mounts row + sentinel, Agents) and verifies every single-char
alphabetic `Key` item is uppercase
Key handlers extended to accept both cases where a footer now shows
uppercase. Most handlers already matched `'e' | 'E'` from batch 11;
the remaining lowercase-only sites (list Q/E/N/D/O, list K/J nav,
editor S/K/J, editor-Mounts A/D/O, file_browser S/H/L nav, picker
J/K) now take `'x' | 'X'`. Behavioural change is nil — Caps Lock
and Shift-held hotkeys now work where they already did at the
footer-advertised case.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* feat(launch): click-to-select in workspace list
Extend `handle_mouse` with row-click selection on the workspace-list
pane. Left-button Down inside the list content area maps to the row
index and updates `ms.selected`; clicks on the seam column still
start a drag (regression guard for batch 14).
Hit-test rules:
- Seam always wins — a click within ±1 column of the current seam
starts a drag regardless of y. This keeps the resize affordance
unambiguous even when the seam overlaps a valid row position.
- Otherwise, clicks inside `[1, seam - 1]` × `[header + 1, body_end - 1]`
(left-pane interior minus borders) convert to a row index via
`mouse.row - (header_height + 1)`.
- The index must be in `[0, sentinel_idx]`; beyond that we silently
drop the click. Row 0 = "Current directory", 1..=N = saved, N+1 =
"+ New workspace" sentinel.
- Clicks outside those ranges (header, footer, borders, right pane)
are ignored.
Layout heights are pulled from two new private consts mirroring
`render::render`'s `Constraint::Length(3)`/`Length(2)`. If the
chrome ever changes shape, both the render and hit-test paths need
updating together.
Double-click = launch is intentionally skipped: crossterm doesn't
emit native double-click events, so implementing it would need
tracking `(last_row, last_instant)` on `ManagerState` with a debounce
window. That's more state-machine than one item in this batch
warrants — left as a follow-up. Single-click-to-select is the
must-have and is fully wired.
Five new tests cover the happy path (row 0, mid-list row, sentinel
row), the negative path (header / borders / right pane / below
sentinel / footer), and the seam precedence regression guard.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
* fix(launch): FileBrowser Esc steps back one level when not at root
Previously Esc always cancelled the modal. If the operator drilled into
a subfolder (via Enter on a plain dir or via the git-prompt
"Pick a subdirectory" path), a stray Esc collapsed the whole picker and
returned to the workspace list. Operators expect Esc to back out one
level — mirroring the behavior of `h` / `←` — and only cancel when
already at root.
Esc now:
- Clears any stale rejected_reason.
- Navigates one level up when cwd != root (sandbox-guarded, same as the
existing root-clamp).
- Cancels the modal only when cwd == root.
Git-prompt Esc is unchanged: it still dismisses only the prompt and
leaves the explorer open at the current cwd.
Footer hint updated from "Esc cancel" to "Esc up/cancel" (matching the
batch 16 uppercase-hotkey convention) — accurate for both drilled-in
and root cases.
Five new tests cover: esc-at-root cancels, esc-in-subfolder navigates
up, esc-three-levels-deep goes up exactly one, esc clears
rejected_reason, and the git-…
donbeave
added a commit
that referenced
this pull request
May 7, 2026
* docs(specs): workspace manager TUI (PR 2 of 3) Design spec for PR 2 of the launcher-workspace-manager series. Adds an interactive workspace manager screen to the jackin launcher — list, create, edit, and delete workspaces without dropping to CLI. Reached via `m` from the existing Workspace picker; Esc returns to the launcher. Launch path stays keystroke-identical. Key design decisions from brainstorming (all settled, no open questions): - Entry model: separate Manager screen on `m` keypress; launch path unchanged. - Editor tab set: General · Mounts · Agents · Secrets-stub. Secrets placeholder locks in the final tab strip so PR 3 fills in the panel without a visual reshuffle. - Text-edit UX: modal push — centered overlay, one reusable TextInput widget. - Staging: explicit save via `s`. Pending changes drive dirty markers; Esc with pending opens Discard/Save/Cancel. - Create flow: mounts-first wizard — file browser for host source, dst auto-defaulted to the same absolute path as src (host-path mirror), workdir picked from mount dsts + ancestors (never free-text), name last with live uniqueness check. - Delete UX: single-line Y/N confirm modal. - Style: reuses jackin's existing digital_rain (src/tui/animation.rs), step_shimmer, spin_wait, and landing-page color tokens from docs/src/components/landing/styles.css. One new area-bounded rain widget extracted from animation.rs. Three new reusable widgets emerge (TextInput, FileBrowser, Confirm) that PR 3's Secrets tab will consume unchanged. All persisted writes flow through ConfigEditor (established in PR 1, merged in #162). Non-goals: per-(workspace × agent) env overrides (PR 3), global mount management (CLI only), agent lifecycle from manager (CLI only), CLI surface changes, CHANGELOG. Co-authored-by: Claude <noreply@anthropic.com> * docs(specs): lock third-party widget choices for PR 2 Amends the workspace manager TUI spec with a Third-party dependencies subsection that names the three ratatui ecosystem crates we'll adopt: - ratatui-textarea (v0.9.x) — single-line TextInput (ratatui-org owned) - ratatui-explorer (v0.3.x) — FileBrowser with folders-only wrapper - tui-widget-list (v0.15.x) — WorkdirPick list mechanics All three require the ratatui unstable-widget-ref feature flag. Rejected with rationale so reviewers don't re-litigate: tui-input (superseded by ratatui-textarea), tui-confirm-dialog / tui-overlay (Confirm modal is cheaper hand-rolled), rat-widget (too opinionated), throbber-widgets-tui / ratatui-cheese (we have spin_wait already), ratatui-toaster (banner is ~30 LOC with step_shimmer), tui-logger (jackin has no log or tracing framework today). Also updates Rollout section — "no new dependencies" was no longer accurate. Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Codex <codex@openai.com>
donbeave
added a commit
that referenced
this pull request
May 7, 2026
* docs(plans): workspace manager TUI implementation plan
Twenty-two-task TDD-shaped implementation plan for the workspace
manager TUI specced in #164. Ordered in seven phases:
1. Foundation — deps + module scaffolds + animation.rs refactor
2. Widgets — Confirm, TextInput, FileBrowser, WorkdirPick, PanelRain
3. State machine — ManagerState, EditorState, CreatePreludeState
4. Render — list view, editor (4 tabs), modal dispatcher
5. Input — modal-first key dispatch with per-stage routing
6. Integration — LaunchStage::Manager wire-in, m keybinding, full
editor + create key handling, ConfigEditor save/create/delete paths
7. Polish — style effects (boot reveal, save shimmer, toast expire),
integration test, final verification + PR
Each task is TDD-shaped: write failing test → run fails → implement →
run passes → commit. Complete code in every implementation step. No
placeholders.
Scope cut documented in self-review: tab-slider and panel-focus-glow
animations from the spec's Style section are omitted; they're cosmetic
and can land in a follow-up PR without rework.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): scaffold workspace manager modules + widget deps
Adds three ratatui ecosystem crates (ratatui-textarea 0.9,
ratatui-explorer 0.3, tui-widget-list 0.15) and enables ratatui's
unstable-widget-ref feature. Creates empty module structures at
src/launch/widgets/ and src/launch/manager/ to land typed setters,
widgets, and state transitions in subsequent commits.
No behavior change.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(tui): extract render_rain_frame for reuse
Separates the per-frame rain rendering from digital_rain's event loop
so the upcoming PanelRain widget can render bounded-area rain without
duplicating the renderer. tick_rain and RainState become pub(crate)
for the same reason. Fullscreen digital_rain is rewritten to delegate
to render_rain_frame. No visible change.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): Confirm widget — Y/N modal
Hand-rolled Y/N confirmation dialog. Case-insensitive, Esc cancels.
~60 LOC + 5 tests. Used by delete-workspace and discard-changes flows.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): TextInput widget — single-line via ratatui-textarea
Wraps TextArea in single-line mode (intercepts Enter and Ctrl+M so
newlines are never inserted). Exposes a ModalOutcome<String> contract:
Enter commits, Esc cancels, everything else passes through to the
textarea for cursor / insert / backspace handling.
Cursor is placed at end of initial text on construction so editing
feels natural (backspace works immediately on prefilled values).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): FileBrowser widget — wraps ratatui-explorer
Folders-only filter, seeded from $HOME by default, adds 's' as
select-current-folder. Delegates all navigation (h/l/j/k/Enter/
Backspace/Home/End/PgUp/PgDn/Ctrl+h) to ratatui-explorer defaults.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): WorkdirPick widget — choice list via tui-widget-list
Derives the pick list from mount dsts + each ancestor up to /, with
labels (mount dst / parent / root). Deduplicates when multiple mounts
share ancestors. Enter commits selected path, Esc cancels.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): PanelRain widget — area-bounded phosphor rain
Wraps tui::animation's RainState engine for rendering into a bounded
Rect. Tick + render are separate so callers control frame rate.
Resizes state when the rect changes shape.
Adds RainState::new(cols, rows) constructor to animation.rs so the
widget can initialize state without duplicating the column/grid setup
that was previously inlined in digital_rain().
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): ManagerState + EditorState + CreatePreludeState types
Defines the top-level state machine per spec § 3: ManagerStage enum
(List / Editor / CreatePrelude / ConfirmDelete), EditorState with
dirty detection and change_count, CreatePreludeState with the
mounts-first wizard step enum, Modal enum with target enums, Toast
type, and constructors. Tests cover WorkspaceSummary derivation,
ManagerState::from_config, EditorState dirty detection, and
CreatePreludeState initial step.
ManagerStage and ManagerState carry a lifetime parameter propagated
from TextInputState<'a> (ratatui-textarea borrow). MountConfig lacks
Ord/Hash so change_count uses linear containment checks rather than
BTreeSet symmetric_difference.
Transitions and key handling are filled in by subsequent tasks.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): create-workspace wizard state transitions
Mounts-first flow: PickFirstMountSrc → PickFirstMountDst → PickWorkdir →
NameWorkspace. Each accept_* method advances the step. default_mount_dst
mirrors the host src path. default_name derives from the dst basename.
build_workspace assembles the final WorkspaceConfig.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): manager list view render
Renders ManagerStage::List: header banner, horizontal-split body
(workspace list + details pane), footer hint. Other stages rendered
by subsequent tasks (12: editor, 13: modal dispatcher).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): editor view render (all four tabs)
Renders Editor stage with General / Mounts / Agents / Secrets-stub
tabs, dirty markers on changed fields, save-count footer. Error
banner overlays the top of the tab body using --landing-danger
(#ff5e7a) for real errors.
Refactors top-level render to let stages declare whether they use
shared chrome (List, future ConfirmDelete) or their own full-screen
layout (Editor, future CreatePrelude).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): modal render dispatcher
Centers a modal Rect at 60x30 percent of the frame and dispatches to
the appropriate widget's render function based on the Modal variant.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): key dispatcher with modal precedence
Scaffolds handle_key with modal-first precedence: if a modal is open
anywhere in the state machine, events route to the modal handler
before per-stage handlers. Full editor + prelude wiring lands in
Tasks 16 / 17; this commit has stubs for those to keep the compiler
happy. List and ConfirmDelete stages are fully wired (navigation,
delete flow via ConfigEditor).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): LaunchStage::Manager + m keybinding
Adds a third launch stage and wires an m keypress from the Workspace
picker to transition into it. run_launch now takes AppConfig by value
+ &JackinPaths so the manager can open ConfigEditor. Footer hint in
the Workspace stage gains 'm manage'.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): editor key handling — tabs, save, discard, field edits
Implements Tab/Shift-Tab navigation between tabs, ↑↓ row selection,
Enter-to-edit (opens modal per field type), Space/* on Agents tab,
a/d on Mounts tab. s triggers save via ConfigEditor::edit_workspace
or create_workspace, with error banner on failure. Esc with pending
changes opens the Discard confirm modal; Esc with clean state returns
to the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): full create-workspace prelude flow
Chains the four modals (file browser → dst TextInput → workdir pick →
name TextInput) through CreatePreludeState. On completion, transitions
to Editor(mode=Create) with everything pre-populated. s in the editor
creates via ConfigEditor::create_workspace.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): manager style effects
Boot reveal on manager entry via tui::animation::digital_rain(400, None).
Save toast auto-expires after 3s. Shimmer: toast text flashes white during
the first 400ms post-show. JACKIN_NO_ANIMATIONS=1 disables the rain
transition.
Co-authored-by: Claude <noreply@anthropic.com>
* test(launch): end-to-end manager delete-workspace flow
Drives manager::handle_key with scripted key events (d, y). Asserts
the workspace is removed from on-disk config, the manager transitions
back to List, and the in-memory workspace list refreshes. Regression
guard against state-machine drift.
Co-authored-by: Claude <noreply@anthropic.com>
* style(launch): quiet clippy / fmt for workspace manager
Fix all clippy warnings introduced by the workspace-manager TUI (~1500
lines of new code): unnested or-patterns, collapsible-if, elidable
lifetimes, default-trait-access, items-after-statements, match-for-
equality, match-for-single-pattern, needless-pass-by-ref-mut,
doc-markdown (missing backticks), missing-const-for-fn, uninlined-
format-args, manual-Debug-non-exhaustive, large-enum-variant (allow),
too-many-lines (allow), unnecessary-trailing-comma, and the associated
fmt diff (11 files reformatted).
No logic changes; all tests pass (workspace_config_crud requires
--test-threads=1 due to pre-existing set_current_dir race).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): replace Workspace picker with manager as initial stage
The old Workspace picker stage is removed. LaunchStage::Manager becomes
the initial stage — jackin opens directly to the manager. Enter on a
workspace launches it (via the existing Agent picker); e opens the
editor; n creates; d deletes; q/Esc exits jackin. The m keybind is gone
— nothing to enter since we are already in the manager.
Esc from the Agent picker returns to the manager list (was: Workspace
picker, which no longer exists).
Also removes the mid-loop digital_rain(400, None) boot reveal that was
fighting with skippable_sleep's raw-mode toggling, which caused arrow
keys to print as raw escape sequences in the manager instead of being
captured by crossterm events.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): render active modals in manager
render_modal was defined but never called. Modals in Editor and
CreatePrelude stages transitioned correctly in state but had no
visible effect on screen — pressing n would silently put the user in
the create wizard with an invisible FileBrowser, making the create
and edit-field flows appear broken.
Also renders the ConfirmDelete variant's confirm modal directly
(ConfirmState on ConfirmDelete is a top-level field, not wrapped in
Modal::Confirm).
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): modal footer hints + stage-aware manager footer
File browser and text input modals were rendering without any hint
about the keys to commit/cancel. Users opening the create workspace
flow saw the folder picker but couldn't progress — pressing Enter
only descended into folders (s is the select key for ratatui-explorer),
and there was no visual cue about the right key.
Adds a one-line phosphor-dim italic footer inside each modal:
- FileBrowser: ↑↓ navigate · Enter open · h/← up · s select · Esc cancel
- TextInput: Enter confirm · Esc cancel
Also makes the top-level manager footer hint stage-aware:
- List stage: existing navigation hint (unchanged)
- CreatePrelude: Create workspace · follow the prompts · Esc cancel
- ConfirmDelete: Y yes · N no · Esc cancel
- Editor: still delegates to render_editor's own footer
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): Esc in create wizard returns to list immediately
Previously pressing Esc inside the first create-wizard modal cleared
the modal but left the state machine stuck in ManagerStage::CreatePrelude
with no modal active — render drew a blank body, requiring a second Esc
to reach the non-modal prelude handler that transitions back to List.
Now the post-modal check distinguishes three outcomes: in-progress
(modal still open), complete (wizard finished with name), and
cancelled (modal cleared without a name). Cancelled transitions to
List in the same input pass.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): wire rename + render all agents on Agents tab
Fixes two spec gaps:
1. Agents tab rendered only currently-allowed agents, so the user
couldn't add agents via Space toggle — non-allowed agents were
invisible. Now iterates config.agents (the full set) and shows
[x] or [ ] per agent based on pending.allowed_agents membership.
Threads &AppConfig through manager::render → render_editor →
render_agents_tab. Also fixes set_default_agent_at_cursor to use
config.agents for cursor-to-agent resolution instead of the
allowed-only list.
2. Workspace rename was a TODO. Now:
- ConfigEditor gains rename_workspace(old, new) using toml_edit's
key-rename (preserves nested tables + array-of-tables). Rejects
empty new name, collision, and missing old name.
- General tab's name row is editable on Enter (in Edit mode) via
TextInput modal.
- apply_text_input_to_pending stashes the name on
EditorState::pending_name.
- save_editor calls rename_workspace before edit_workspace when
pending_name differs, then updates editor.mode so subsequent saves
target the new name.
- change_count + is_dirty + render dirty marker all track the rename.
Tests: three new unit tests on ConfigEditor::rename_workspace covering
happy path (nested tables preserved), collision rejection, and empty-
name rejection.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): FileBrowser s commits cwd, not highlighted entry
Pressing s in an empty or file-only folder previously committed the
highlighted entry, which in such a folder is '../' — so the user got
the parent directory, not the folder they were viewing. This was
especially bad for newly-created empty workspace source folders.
Now s commits the explorer's current working directory via
FileExplorer::cwd() (ratatui-explorer 0.3.x). User intent is preserved:
'I've navigated to this folder — select it.' Footer hint updated from
's select' to 's use this folder' to reflect the semantics.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): render active-row cursor in editor tabs
EditorState::active_field tracked the cursor but render functions
didn't display it — users couldn't tell which row Enter / Space / * /
a / d would target. Add a ▸ prefix and phosphor-green bold to the
selected row across all three tabs (General, Mounts, Agents).
Also clamp Down-arrow to the last valid row so the cursor can't run
off the end of the visible content, and thread &AppConfig through to
handle_editor_key's Down handler so it can size the Agents tab's
row count correctly.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): UX polish pass on manager create/edit flows
Nine polish fixes reported after live walkthrough:
1. TextInput/Confirm modals were 30%-of-screen tall, rendered as big
empty boxes around a single line. Now variant-aware: inputs/confirms
are 5-6 rows fixed; file browser and workdir pick stay taller for
their scrolling lists.
2. 'last used' row hidden in Create mode (no history exists).
3. 'default agent' row hidden in Create mode (no agents picked yet).
4. Footer hint is now row-contextual: 'Enter rename' on name row,
'Enter pick workdir' on workdir row, 'a add / d remove' on mounts,
'Space toggle / * set default' on agents, nothing on read-only
rows. Base hint says 's save workspace' (was 's save') for clarity.
5. File browser gets a prominent outer block titled '<cwd> · press
[S] to use this folder' — the select affordance was previously
buried in a dim footer line.
6. Mount rows collapse 'src → dst' to just 'path' when src == dst
(host-path-mirror default — redundant arrow gone).
7. Mounts tab '+ Add / − Remove selected' footer uses white-bold for
the action words to distinguish from the mount list.
8. Agents tab gets a top banner clarifying empty = 'all allowed'
semantics vs non-empty = custom allow-list.
9. Read-only rows (last used) no longer advertise Enter in the footer.
Also fix max_row_for_tab in input.rs: Create mode General tab only
has 2 rows (name read-only + workdir), not 4.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): Confirm modal shows Y/N as styled buttons
Previously the modal rendered '[Y]es · [N]o (default) · Esc cancel' as
inline text inside a tall 30%-of-screen box, which looked like a
multi-line textarea. Now:
- Modal is compact (6 rows)
- Yes/No render as inverted-video buttons, centered
- No (default) uses white-on-black to distinguish as default action
- Esc cancel moves to a dim italic footer hint at the bottom
- Prompt text stays bold-white at the top
Enter intentionally unbound — destructive confirms should not commit
on accidental Enter presses.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): remove nested borders in FileBrowser modal
ratatui-explorer's widget renders its own bordered block with the CWD
as title. The prior polish pass added an outer block with 'press [S]
to use this folder' — the result was double borders, ugly and
confusing.
Drop the outer block. Show the 'press [S]' affordance as a bold-white
centered line ABOVE the explorer (no border), and keep the dim
navigation hint as a line BELOW. The explorer's own cwd-titled block
stays — no nesting.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): Confirm modal gains Tab focus + Enter commits focused
Adds standard confirmation-dialog UX: Tab / Shift+Tab / ←→ / h/l cycle
focus between Yes and No; Enter commits the focused button. Default
focus is No (destructive action protection — accidental Enter won't
commit Yes). Y/N direct shortcuts still work regardless of focus.
Visual: focused button gets white bg + black text + bold; unfocused
gets phosphor-green bg. Footer hint updated to mention Tab + Enter.
Modal grows from 6 to 7 rows for a second spacer between buttons
and hint (was visually cramped).
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): shrink FileBrowser + WorkdirPick modal heights
Prior sizing was 60 rows for FileBrowser and 40 rows for WorkdirPick —
effectively fullscreen on a typical 40-50 row terminal. Tight 20 and
12 rows fit comfortably and still show enough entries without the
modal swallowing the whole screen.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): + Add mount is a selectable sentinel row
Mirrors the + New workspace sentinel in the manager list. The Mounts
tab now renders + Add mount as a real selectable row at the end of
the list, selected via ↑↓, activated via Enter. Visual treatment is
white bold (distinguishing it from the green mount rows).
- max_row_for_tab reports len() (mount count + sentinel index) for
Mounts so ↓ can reach the sentinel.
- remove_mount_at_cursor is a no-op on the sentinel (guard already existed).
- a (anywhere on the tab) still works as a quick-add shortcut.
- Contextual footer hint differentiates between 'on a mount row'
(d remove · a add) and 'on the sentinel' (Enter add · a add).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): Agents tab shows [all] / [custom] status badge
Replaces the implicit 'empty list = all allowed' with an explicit
status line at the top of the Agents tab:
Allowed agents: [ all ] (when allowed_agents is empty)
Allowed agents: [ custom ] (3 of 5 allowed) (when non-empty)
The badge is an inverted-video token (phosphor-green bg for 'all',
white bg for 'custom') making the current mode immediately visible.
The agent list below stays as a checklist — toggling updates the
status badge live.
Cursor semantics also shift: cursor is now 0-based into config.agents
(no more header-offset-by-one). toggle_agent_allowed_at_cursor and
set_default_agent_at_cursor are updated accordingly. max_row_for_tab's
Agents arm drops to len()-1.
set_default_agent_at_cursor now also auto-allows the agent being set
as default (was previously a no-op if the agent wasn't already in
allowed_agents).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): Mounts tab shows folder / git · <branch>
Adds a mount_info helper that inspects the host-side src path on
render: checks for .git as dir or submodule-gitfile, reads HEAD, and
reports the current branch (or detached short-sha). Renders next to
each mount row as dim italic metadata:
/Users/…/repo (rw) · git · main
/Users/…/scratch (rw) · folder
/Users/…/gone (rw) · missing
Six unit tests cover: missing path, plain folder, normal repo with
branch, detached HEAD, submodule .git file, label formatting.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): Save/Discard/Cancel modal + richer details pane
Two UX upgrades:
1. Exit-with-changes now offers three explicit choices instead of
binary 'Discard Y/N'. New SaveDiscardCancel modal with three
buttons (Save / Discard / Cancel), Tab cycles focus, Enter commits
the focused option. S/D/C/Esc shortcuts work regardless of focus.
Default focus is Cancel (safest). Save intent triggers ConfigEditor
save → list; Discard just drops pending; Cancel keeps the editor.
2. Manager list's details pane now shows the full mount list (with
folder / git · <branch> labels, same as the Mounts tab) and the
allowed-agents list (or 'any agent' when unrestricted). Title drops
the duplicate workspace name since the list selection already shows
it.
5 new unit tests on SaveDiscardState covering focus cycling and key
shortcuts.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): restrict FileBrowser to \$HOME, rename main title
Four UX fixes:
1. Main manager screen title 'manage workspaces' → 'workspaces'
(the screen does more than manage — launch, create, edit, delete).
2. FileBrowser modal goes fullscreen (100% x 100%) so the main chrome
doesn't peek through and confuse the visual.
3. FileBrowser now:
- Starts at \$HOME (already did)
- Excludes Library, Applications, Movies, Music, OrbStack, Pictures
from the listing via filter_map
- Clamps cwd back to \$HOME if the user escapes above it via
set_cwd() (ratatui-explorer 0.3.x has this method)
- Rejects \$HOME itself as a workspace source
- Rejects ~/.jackin/* (jackin's reserved data area)
4. Rejected selections show an inline red error banner
(#ff5e7a) above the explorer. Cleared on next keypress.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): display paths as ~/… via shorten_home
Paths starting with $HOME now render as '~/...' in the TUI:
General tab workdir, Mounts tab rows, details pane mounts/workdir,
WorkdirPick choices. Consistent with jackin's existing shorten_home
helper (already used elsewhere in the launcher).
Paths stored on disk are unchanged — this is display-only.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): mount table formatting + FileBrowser resize/colors
Two fixes:
1. Mount lists in the details pane and Mounts tab render as an
aligned 3-column table (path, mode, type) instead of a free-form
line where the '(rw)' tag and type metadata floated at variable
positions. shorten_home applied to paths consistently via the
shared format_mount_rows helper, which is called from both
render_details_pane and render_mounts_tab.
2. FileBrowser modal goes from fullscreen (100%) to 70%x70%, letting
the surrounding chrome show again so the dialog reads like a
dialog, not a whole screen. Theme configured to use jackin's
phosphor palette (green text, bright-phosphor highlight, shortened
CWD title via shorten_home in a dynamic with_title_top closure).
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): drop Agents 'default' column + cap workspace list height
1. Agents tab header 'allowed? · default · agent' → 'allowed? · agent'.
The star marker next to the agent name already indicates default;
the dedicated column was empty for every non-default row.
2. Manager list body now caps at content height (workspace count + 2
border rows + 1 sentinel row) instead of filling the whole frame.
5-6 workspaces no longer render in a box that looks two-thirds
empty.
Co-authored-by: Claude <noreply@anthropic.com>
* Revert "fix(launch): cap workspace list height to content"
The height cap made the space below the boxes visibly empty, which
reads worse than the previous full-height boxes. User feedback:
'before it was better when it was using the whole vertical space.'
Keeps the Agents tab header change from the same original commit
(3fdab9f3) — only the list-body sizing is reverted.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): hide FileBrowser .. entry at $HOME root
Previously the '../' entry was always shown in the file browser.
When the user was at $HOME, selecting it would escape the sandbox
(and was then clamped back by set_cwd) — confusing and cluttered.
Now the filter hides '..' when its target path is outside the root
subtree. At $HOME the entry disappears; at any subfolder of $HOME
it still appears so the user can navigate back up.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): hide empty right pane, split details, clickable git links
Three UX improvements:
1. When the cursor is on '+ New workspace' in the manager list,
the right details pane is hidden entirely — the list takes full
width. No more empty bordered box for the sentinel row.
2. Details pane split into three stacked sub-panels: General (workdir
+ last used), Mounts (tabular with header row), Agents (list or
'any agent'). Each has its own bordered mini-block with phosphor-
dark border and white-bold title. The outer 'Details' block is gone.
3. Git branch URL resolution wired up: inspect() now parses
<git_dir>/config to find the origin remote and derives a web URL
(GitHub, GitLab, generic HTTPS/SSH). MountKind::Git gains a
web_url: Option<String> field; MountKind::labeled_hyperlink() wraps
the branch name in OSC 8 escape sequences for supported terminals
(iTerm2, kitty, WezTerm, Alacritty, modern Terminal.app).
OSC 8 fallback: ratatui's Paragraph widget strips raw ESC bytes, so
the render path continues to call label() (plain text). The
hyperlink infrastructure (labeled_hyperlink, osc8_link, web_url) is
retained for a future raw-terminal-write path. Both are annotated
#[allow(dead_code)] with an explanatory TODO.
5 new unit tests on remote-URL parsing (GitHub SSH, GitHub HTTPS,
ssh:// protocol, GitLab SSH, config-file parse). All 566 tests pass.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): polish FileBrowser, WorkdirPick, and mount-dst prompt
- FileBrowser entries now render white instead of phosphor-green so the
bright-green highlight is the unambiguous focus indicator.
- TextInput prompts for mount destination say "destination (default:
same as host path)" instead of the internal "Mount dst" phrasing.
- WorkdirPick lines are laid out as a table: the path column is padded
to the widest choice so the dim+italic label column (`(mount dst)`,
`(parent)`, `(root)`, `(home)`) lines up cleanly.
- WorkdirPick filters `/` and the literal parent of `$HOME` (e.g.
`/Users` on macOS, `/home` on Linux) from the choice list — those
paths are never useful workdir targets.
- When a path is exactly `$HOME`, label it `(home)` instead of
`(parent)` so the workspace operator sees a recognisable name.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): FileBrowser s commits highlighted folder
Previously `s` always committed the explorer's cwd, which meant the
operator had to press Enter to navigate into the target folder before
committing — even though the folder was already highlighted and the
target of a single Enter press.
Reading `FileExplorer::current()` lets us commit the highlighted entry
directly when it is a real child directory. The synthetic `../`
parent-link row and the empty-listing case both fall back to the cwd,
preserving the previous behaviour for those edge cases.
The existing $HOME and `~/.jackin/*` rejection rules apply to whichever
path is chosen as the commit target.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): keep General-tab labels white; note agent-hyperlink TODO
- render_editor_row and render_editor_readonly_row no longer shift the
label column to phosphor-green when the row is focused. Labels stay
white (bold when focused); values keep their phosphor colouring for
editable rows and dim phosphor for read-only rows.
- Read-only rows used to render everything in phosphor-dim, which made
the editor view look washed-out. They now match the editable-row
label treatment (white) with a dim value + italic "(read-only)"
suffix, giving the operator a cleaner signal-to-noise ratio.
- Added a TODO in render_agents_subpanel mirroring the existing
labeled_hyperlink() note in render_mounts_subpanel: ratatui's
Paragraph strips OSC 8 ESC sequences, so agent-name → GitHub links
stay plain-text until a raw-write path exists.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): gate editor save on mount-collapse plan
The editor used to write configs straight to disk via
`ConfigEditor::edit_workspace` (or `create_workspace`), which meant the
operator could save a workspace with overlapping mounts like
`~/Projects` and `~/Projects/test`. The CLI rejects this unless you
confirm or pass `--prune`; the TUI now does the same.
Flow:
- On `s`, run `workspace::planner::plan_edit` (Edit) or `plan_create`
(Create) against the pending mount set.
- `CollapseError::{ReadonlyMismatch, ChildUnderExistingParent}` ->
error banner, no write.
- Pre-existing collapses only (no edit-driven) -> error banner
referencing `jackin workspace prune <name>`. The operator can't fix
these from the editor alone and the CLI prune command already exists
for this case.
- Edit-driven collapses -> open a `Modal::Confirm` with a
`ConfirmTarget::SaveCollapse` target, listing each child/parent pair
in the same wording as the CLI. On Yes, the save re-enters with
`EditorState::collapse_approved = true` and commits the collapsed
mount set via `plan.effective_removals` / `plan.final_mounts`. On No
/ Esc, pending mounts are kept intact so the operator can edit by
hand.
Pattern: a boolean flag on `EditorState` + a new `ExitIntent::RetrySave`
variant so the confirm-yes path reuses the existing modal-exit routing
but stays in the editor on success (rather than bouncing to the
workspace list, which is what `ExitIntent::Save` does). The plan
itself is not stashed; it is cheap to recompute on re-entry.
The `Confirm` widget now grows its prompt region to match the number
of lines in `state.prompt`, and `render_modal` sizes the outer rect
via `confirm::required_height` so multi-line collapse summaries render
without clipping.
Tests (5 new):
- `save_editor_opens_confirm_on_edit_driven_collapse`
- `confirming_collapse_writes_collapsed_set`
- `cancelling_collapse_keeps_pending_mounts_intact`
- `readonly_mismatch_produces_error_banner_no_write`
- `pre_existing_collapse_produces_prune_error_banner`
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): structured footer with per-item styling
Introduce a `FooterItem` enum (Key / Text / Dyn / Sep / GroupSep) and a
shared `render_footer` that emits spans with a consistent palette:
- Key glyphs (↑↓, Enter, e/n/d/q, Tab, Esc, S, Y/N, *, Space) render in
WHITE + BOLD so they pop out of the legend.
- Action labels ("launch", "edit", "new", …) render in PHOSPHOR_GREEN.
- Inline dots (·) render in PHOSPHOR_DARK as a faint separator.
- A GroupSep (three spaces, no style) introduces a wider visual gap
between logical groups — navigation, per-row actions, and exit.
Migrate every footer call site to this scheme:
- `manager/render.rs` List / CreatePrelude / ConfirmDelete / Editor
footers build `Vec<FooterItem>` explicitly so the grouping is
deliberate per stage.
- Agent-screen footer in `launch/render.rs` uses the same inline spans.
- Modal-local hints inherit the scheme (file_browser navigation + "[S]
to use this folder" affordance, text_input "Enter confirm · Esc
cancel", confirm "Tab cycle · Enter confirm · Y yes · N no", and
save_discard).
Add unit tests covering the span-style mapping per variant plus
smoke tests for the List and ConfirmDelete stage footers.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): keep right pane visible on '+ New workspace' sentinel
Batch 7 expanded the list to full width when the cursor landed on the
sentinel row. The operator wants the 45/55 split preserved — the layout
should not shift as the cursor moves — with the right pane rendered as
an empty bordered block (same PHOSPHOR_DARK border as the General /
Mounts / Agents sub-panels) when there is no workspace to describe.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): align mount-table header with data columns
The mount-table header was a hardcoded string (" path<23 spaces>mode<3
spaces>type") while data rows computed their path column width
dynamically from the widest row. When paths were shorter than 23 chars
the header appeared drifted relative to the data; when they were longer
the header's "mode" column collided with the data's mode column at a
different offset.
Share the column-width computation between the header and data rows:
- Extract `mount_path_width` which returns max(row_path, "path".len(),
10) so the header and data always use the same column boundary.
- Add `render_mount_header(path_w)` that uses the same format string as
the data rows, then have both the read-only details subpanel and the
editor Mounts tab consume it.
- Pin the `mode` column to a shared `MOUNT_MODE_COL_WIDTH = 4` constant
(covering "mode" as well as "rw"/"ro" + trailing space) so it no
longer over-pads inconsistently.
Add unit tests that build mount rows with mixed path lengths and assert
the header's "mode" column starts at the same character index as each
data row's "mode" column.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): describe workspace concept on '+ New workspace' pane
Replace the empty bordered block shown to the right of the manager list
when the sentinel row is focused with a two-panel description pulled
from the "What is a workspace?" / "Why save a workspace?" sections of
the workspaces guide. Keeps the right-hand real estate useful for
first-time operators and matches the General/Mounts/Agents sub-panel
chrome for visual consistency.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): restore 'Current directory' row in workspace manager
Before the TUI redesign the launcher's first row was a synthetic
"Current directory" choice that let operators launch an agent against
cwd without saving a workspace. The manager's rewrite dropped it; this
reinstates it as row 0 of the list with the right-pane summary, the
cwd-aware preselect, and the launch wiring that matches the old
behaviour.
Row layout (enforced by ManagerState::from_config, render_list_body,
and handle_list_key):
row 0 → synthetic "Current directory"
rows 1..=N → saved workspaces
row N+1 → "+ New workspace" sentinel
Edit (`e`) and Delete (`d`) are rejected on row 0 with a toast. Enter
on row 0 emits a new InputOutcome::LaunchCurrentDir; the run-loop
routes it through the same agent-picker transition as LaunchNamed,
reusing LaunchState::workspaces[0] (the CurrentDir choice built by
LaunchState::new). Preselect reuses find_saved_workspace_for_cwd so
TUI and CLI agree on "which workspace am I in?".
The right pane branches on row 0 → render_current_dir_details_pane
(dedicated renderer; no last-used row, no edit affordance, "any
agent"). The sentinel description pane lands in the same commit's
sibling already; saved-workspace rows continue to use the shared
render_details_pane with `workspaces[selected - 1]`.
Tests added:
- manager_preselects_saved_workspace_matching_cwd
- manager_preselects_current_directory_when_no_saved_matches
- manager_current_directory_is_first_row
- current_directory_row_rejects_edit_and_delete
- enter_on_current_directory_returns_launch_current_dir
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): polish mount-header gap, modal titles, and FileBrowser size
- Mount table header: add two-space gutter between `mode` and `type`
so the header no longer reads "modetype". Data rows now emit the
matching two-space gap so the `type` column aligns in both the
read-only Mounts subpanel and the editor Mounts tab.
- Text-input + Workdir-pick modal block titles render WHITE + BOLD to
match the General/Mounts/Agents block titles on the main screen.
Confirm + SaveDiscard already use WHITE+BOLD — left untouched.
- WorkdirPick path values render WHITE (the `(mount dst)`/`(parent)`/
`(home)`/`(root)` label suffix stays PHOSPHOR_DIM italic).
- FileBrowser modal height drops from 70 absolute rows to 22 so it
no longer eats the whole screen. Width stays at 70%.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): classify git mounts by host and relabel GitHub remotes
- Introduce `GitHost { Github, Other }` on `MountKind::Git` so the
render path can tell which remotes have an "open in browser"
affordance. `inspect` populates this from `parse_remote_origin_url`:
SSH `git@github.com:`, HTTPS `https://github.com/…`, and
`ssh://git@github.com/…` all resolve to `Github`; anything else
(self-hosted, GitLab, no remote, unparseable URL) falls through to
`Other`.
- `remote_to_web` now returns `Some(url)` only for GitHub hosts and
`None` for everything else — it no longer synthesises `gitlab.com`
URLs. Non-GitHub remotes keep `web_url: None` on the `MountKind`.
- `MountKind::label()` renders `github · {b}` / `github · detached {sha}`
/ `github` for GitHub hosts and keeps the generic `git · …` prefix
for `Other`. `MountKind::Folder` / `Missing` unchanged.
- `remote_to_web_gitlab` test re-purposed to assert GitLab (and other
non-GitHub hosts) now return `None`. New tests for the GitHost split
via `inspect` and for the `remote_points_at_github` predicate covering
all three URL forms + a GitHub-lookalike subdomain rejection.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): 'o' key opens highlighted GitHub mount in the browser
- Add the `open` crate (5.x) so the editor can launch the system
browser without blocking the TUI (`open::that_detached`).
- Wire `o` into the editor's Mounts tab: when the cursor is on a
mount row whose source resolves to a GitHub-hosted repo with a
web URL, pressing `o` opens that URL in the operator's default
browser. Non-GitHub / folder / missing mounts emit an "no GitHub
URL for this mount" toast so the hint is discoverable; the sentinel
"+ Add mount" row is a silent no-op.
- `contextual_row_items` now composes an `o open in GitHub` item
onto the existing `d remove · a add` pair when the current row is
a GitHub mount. List-view mounts pane is unchanged — the `o` key
only binds in the editor.
- Tests: `github_mount_row_includes_open_in_github_hint` and
`non_github_mount_row_omits_open_in_github_hint` pin the footer
composition. No unit test for the browser side-effect itself.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): flag git repos in FileBrowser and offer mount-or-dive prompt
Part A — directory listing:
- `annotate_file` (new filter_map body) stats each directory for a
`.git` child and appends U+25A3 (▣) to the display name when present.
Works for plain clones (`.git` is a directory) and submodules (`.git`
is a file containing `gitdir: …`). Single stat per entry — no
recursive walk.
Part B — Enter on a git-repo row:
- New `GitPromptFocus { MountHere, EnterIn, Cancel }` + two fields on
`FileBrowserState` (`pending_git_prompt`, `pending_git_focus`) drive
an in-widget confirm overlay. Enter on a git-repo row opens the
prompt; Tab/←→/h/l cycle focus; Enter commits the focused option;
M/E/C are direct shortcuts; Esc dismisses the prompt without
cancelling the browser.
- MountHere commits the repo path through the same sandbox rules as
`s` (rejects root / `~/.jackin/*`). EnterIn navigates into the repo
via `explorer.set_cwd` (avoids re-posting Enter, which would re-open
the prompt). Cancel just clears state. Non-git folders keep their
usual Enter-navigates-in behavior.
- Overlay renders as a centred 3-button bar inside the explorer area
so the listing stays visible as context. Phosphor palette + focus
styling mirrors `confirm.rs`/`save_discard.rs`; button ring copied
locally rather than cross-importing between widgets.
- Footer legend swaps to `Tab cycle · Enter confirm · Esc cancel`
while the prompt is active.
Tests cover: marker on `.git`-dir and submodule-`.git`-file cases, no
marker on plain folder, Enter opens prompt on repo row, MountHere
commits the path, EnterIn navigates in and clears prompt, Cancel
clears without cwd change, Esc dismisses prompt without cancelling
browser, plain-folder Enter navigates as before, and the `M` shortcut
commits regardless of current focus.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): add mount-destination choice modal widget
Introduces the `mount_dst_choice` widget and wires a new
`Modal::MountDstChoice` variant into the manager's modal enum plus
render dispatcher. No input behaviour changes yet — follow-up commits
swap the Editor and Prelude FileBrowser→TextInput chains to route
through this modal.
The widget is the 3-button focus-ring pattern pioneered by
`save_discard`: default focus on `OK`, Tab/BackTab cycling, and single-
letter shortcuts (`o`/`e`/`c`). Default on `OK` because the common case
is `dst = src`, so an accidental Enter commits that without surprise.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): route editor add-mount through destination choice modal
The FileBrowser→TextInput chain in the Editor's Mounts tab assumed
every operator wanted to edit the destination path. In practice, 95%
of mounts commit with dst = src. Swapping in the new MountDstChoice
modal makes the common path a single Enter press and keeps the old
behaviour one keystroke away via `Edit destination`.
`apply_file_browser_to_editor` now opens MountDstChoice instead of
pushing a provisional mount plus TextInput. The actual push happens
in the MountDstChoice commit handler:
- OK: push MountConfig { src, dst = src, rw }, close modal.
- Edit destination: push the provisional mount (as today) and open
Modal::TextInput{MountDst} pre-filled with src. The existing
TextInputTarget::MountDst handler overwrites the provisional dst.
- Cancel / Esc: close the modal, leave pending.mounts untouched.
Behavioral tests pin all three paths and guarantee no mount is
pushed until the operator commits in the choice modal.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): route create-prelude add-mount through destination choice
Mirrors the editor-side change: the Create wizard's FileBrowser step no
longer assumes the operator wants to edit the destination. Instead, the
prelude now opens MountDstChoice after FileBrowser commits, offering
the fast `OK` path that skips TextInput entirely.
Both paths (OK and TextInputDst commit) share a new helper
`prelude_advance_to_workdir_pick` so the downstream WorkdirPick stage
receives the same staged mount regardless of whether the operator
edited the destination. This keeps the chain FileBrowser → (choice) →
WorkdirPick → TextInput(Name) intact for the `OK` shortcut.
Cancel on MountDstChoice matches today's Esc-during-TextInput
behaviour: close the modal, leave the prelude state alone so the
outer dispatcher treats it as a wizard-cancellation and returns to
the manager list.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): extract mount-dst-choice dispatch to keep clippy happy
The inline `Modal::MountDstChoice` arm inside `handle_editor_modal`
pushed the function above clippy's 100-line ceiling. Extract the
outcome dispatch into `dispatch_editor_mount_dst_choice` and tidy the
helper's doc comment so `TextInput` doesn't trip the missing-backticks
lint. No behavioural change.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): strip trailing slash in FileBrowser name filter
ratatui-explorer appends `/` to directory names at runtime, so the
filter in `annotate_file` was comparing `"Library"` against `"Library/"`
and silently letting every excluded entry render. Same bug let `..`
through on the sandbox-escape check. Normalize with
`trim_end_matches('/')` before matching, and harden the `s`/Enter
paths + default key dispatch to guard against empty listings (the
fixed filter can now produce an empty `files()` which made
`current()` and nav-key dispatch panic inside ratatui-explorer).
Co-authored-by: Claude <noreply@anthropic.com>
* polish(launch): simplify Destination modal title
Rename the mount-destination TextInput label from
`destination (default: same as host path)` to plain `Destination`.
The parenthetical hint is redundant after batch 12: the TextInput
only opens when the operator explicitly picks "Edit destination"
on the MountDstChoice modal, so they're already in deliberate-edit
mode with the src pre-filled as the default.
Also capitalizes the title to match the other modal block titles
(Confirm, Unsaved changes, Mount destination, Git repo detected,
Workdir pick, Rename workspace, Name this workspace). No other
titles needed changes — the audit was clean.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): bind Left/Right to prev/next tab in Editor
Extend `handle_editor_key` so Right matches Tab (forward cycle) and
Left matches BackTab (reverse cycle). Wrap-around behavior mirrors
the existing Tab contract: General → Mounts → Agents → Secrets →
General, and symmetrically for reverse.
Modal-open precedence is already guarded by the early-return in
`handle_key` — Left/Right continue to feed into modal handlers
(Confirm, SaveDiscard, MountDstChoice) when a modal is active.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): wire list-view `o` to open workspace GitHub mounts
Extend the `o` key beyond the Editor's Mounts tab to the workspace
list view. On a saved workspace row:
0 GitHub mounts → toast "no GitHub URLs for this workspace"
1 GitHub mount → open::that_detached immediately
≥2 GitHub mounts → open a new GithubPicker modal; Enter commits
the highlighted URL to open::that_detached.
Row 0 (Current directory) and the `+ New workspace` sentinel toast
`no workspace selected` for discoverability. The list footer now
surfaces `o open in GitHub` only on rows whose workspace resolves
to ≥1 GitHub-hosted mount.
Adds:
- new `widgets/github_picker.rs` widget (title-styling and tab-list
pattern mirror WorkdirPick so the modal feels native);
- `Modal::GithubPicker { state }` variant, with arms closed in the
render-size switch, `handle_editor_modal` (defensive cancel), and
a new `handle_list_modal` dispatcher;
- `list_modal: Option<Modal<'a>>` slot on ManagerState — list-view
modals weren't previously anchored anywhere; Editor/CreatePrelude
keep their per-stage modal slots unchanged;
- `resolve_github_mounts_for_workspace` helper, shared by the input
handler and the render-side footer-hint guard.
Piggybacks a one-line clippy fix in file_browser.rs
(`iter().any(|x| *x == bare)` → `EXCLUDED.contains(&bare)`) that
surfaced after the trailing-slash filter landed.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): drop cwd suffix from Current directory row label
The list row for the synthetic "Current directory" choice used to read
`Current directory (~/Projects/foo)`. The right-pane details already
show the cwd on the `workdir` line, so the parenthetical suffix is
duplicate visual load. Render just `Current directory`.
Row 0 keeps its WHITE colour so the synthetic choice still visually
separates from the phosphor-green saved workspaces below it.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): align mount-table type column with header
MOUNT_MODE_COL_WIDTH was 2, matching the literal width of rw/ro but
leaving a 2-char gap before the data row's kind column versus the
header's 4-char "mode" label. Header and data rows shared the same
"{mode:<mw} type" format string but MOUNT_MODE_COL_WIDTH no longer
matched the header label length, so `type` and its data (e.g. "folder")
rendered at different offsets.
Pin MOUNT_MODE_COL_WIDTH to 4 so rw/ro pad to the header's "mode"
width. Both the header and data emit the same two-space gutter before
the `type` column, so the kind label lines up with the header offset.
Extend the existing gap-between-mode-and-type test to additionally
assert that `header.find("type") == data.find("folder")` — the
type-column offset must match for a row with a plain folder mount.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): drop dead OSC 8 hyperlink scaffolding
The `o`-opens-in-system-browser path (via open::that_detached)
supplanted the aspirational OSC 8 hyperlinks-in-terminal route. The
OSC 8 helpers were already #[allow(dead_code)] — remove them:
- `osc8_link()` — wrapped text in OSC 8 ESC sequences;
- `MountKind::labeled_hyperlink()` — built a GitHub-linked label
from a branch/sha + url;
- their associated NOTE blocks on render.rs (mounts subpanel) and
the agents-hyperlink TODO next to render_agents_subpanel.
The `web_url: Option<String>` field stays — the `o` key consumes
it to open the branch URL. Likewise `remote_to_web`,
`parse_remote_origin_url`, and the `GitHost::Github` classification
are all still in the live path.
Clippy baseline (4) unchanged; no tests touched.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): mouse-draggable list/details divider
Add a draggable seam between the workspace list pane and the details
pane in the manager TUI. Click-and-drag on the seam column (within ±1)
resizes the split; the percentage is clamped to [20, 80] so neither
pane can be starved.
Mechanically:
- ManagerState gains `list_split_pct: u16` (default 45) and
`drag_state: Option<DragState>`. `clamp_split` + split-range consts
live alongside. `render_list_body` reads `list_split_pct` instead
of the hard-coded 45/55.
- `src/launch/mod.rs` enables `EnableMouseCapture` after entering the
alternate screen and `DisableMouseCapture` in the terminal guard's
Drop. Side-effect: the terminal's native click-drag text selection
stops working while the TUI is running — hold Shift (Terminal.app,
iTerm2) or Option (iTerm2) to bypass. Documented inline.
- The run-loop now matches on `Event::{Key, Mouse, _}` (was a bare
`if let Event::Key`). Mouse events in the Manager stage dispatch
to a new `manager::input::handle_mouse` with the current terminal
size as a `ratatui::layout::Rect`.
- `handle_mouse` hit-tests the seam, captures a `DragState` anchor
on `Down(Left)`, updates `list_split_pct` on `Drag(Left)`, and
clears the anchor on `Up(Left)`. It also gates on List stage, no
open list-modal, and `term_size.width >= 40`.
Unit tests (8 new, pure state manipulation — no ratatui loop):
- mouse_down_on_seam_starts_drag
- mouse_drag_updates_split_pct
- mouse_drag_clamps_to_min_and_max
- mouse_up_ends_drag
- mouse_down_far_from_seam_does_not_start_drag
- drag_ignored_when_list_modal_open
- drag_ignored_on_non_list_stage
- drag_ignored_when_terminal_too_narrow
Clippy baseline (4) unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): rename current-dir pane first block to "General"
The synthetic "Current directory" row has a right-pane first block titled
" Current directory ", but the left-list row label already conveys that
context. Rename to " General " to match the saved-workspace details pane
(General / Mounts / Agents) so both panes use the same three sub-panel
titles.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): remove phantom empty row in current-dir Mounts block
`render_current_dir_details_pane` hard-coded the Mounts block at
`Constraint::Length(5)`, which over-allocated by one row for the
single-mount current-directory case and left a visible empty line
inside the block border.
Extract the height formula from `render_details_pane` into a shared
`mount_block_height` helper (2 borders + 1 header + max(1, N) data rows,
clamped to 12) and use it from both pane renderers so the two paths
produce identically-tight Mounts blocks.
Covered by four regression tests pinning the formula for the empty,
single, multi, and many-mount cases.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): align General sub-panel content with Mounts/Agents
The General sub-panel (on both the saved-workspace pane and the
current-directory pane) rendered its `workdir`/`last` rows flush against
the block's left border, while the Mounts and Agents sub-panels already
used a two-space indent. The mismatch gave the right pane a jagged left
edge across the three stacked blocks.
Add the same two-space prefix to the General rows on both panes. The
convention is pinned by a new `SUBPANEL_CONTENT_INDENT` constant and
two visual regression tests that render each sub-panel to a
`TestBackend` buffer and assert the first visible character of row 0
sits at that indent relative to the block's left border. Covers the
"any agent" fallback and the starred-default-agent row explicitly.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): follow worktree commondir for GitHost detection
Git worktrees are checkouts whose `.git` is a file pointing at
`<main>/.git/worktrees/<name>`. That per-worktree gitdir owns HEAD but
has no `config` of its own — the shared config (including the remote
URL) lives at the target of a `commondir` pointer. The previous
`resolve_git_dir` stopped at the worktree-specific gitdir, so
`resolve_host_and_url` read nothing, `GitHost` defaulted to `Other`,
and the label rendered as `git · branch` instead of `github · branch`
for every worktree of a GitHub-hosted repo.
Split resolution into `resolve_gitdirs`, which returns a pair:
- `work_dir` — owns HEAD (worktree-specific for worktrees, identical
to `config_dir` for plain clones and submodules).
- `config_dir` — owns the remote URL (follows `commondir` when present,
handling both relative and absolute pointer forms).
`inspect` now parses HEAD from `work_dir` and the remote URL from
`config_dir`, so the host is re-classified correctly for worktrees
without perturbing the submodule path.
Covered by three new tests:
- `worktree_gitfile_resolves_to_commondir` (relative commondir)
- `worktree_commondir_with_absolute_path`
- `submodule_gitfile_still_resolves_host_end_to_end` (regression guard
for submodules — HEAD + config co-located, no commondir)
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): drop FileBrowser affordance banner
The "press [S] to use this folder" banner above the explorer is redundant
with the `S select` footer hint below it, and inconsistent with other
modal styling (no other modal has a top banner). Drop it and its layout
constraint so the explorer shifts up by one row.
The `rejected_reason` banner (shown when the operator picks $HOME itself
or a `.jackin/` path) stays — it is functional error feedback, not an
affordance hint.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): prefix git marker, "repo" -> "repository" in UI
Three small FileBrowser/git-prompt polish items:
- Prepend the git-repo marker instead of appending. Changed the glyph
from U+25A3 (white square with black small square) to U+2387
(alternative key symbol — reads as a branch) and moved it to the head
of the directory name so the eye lands on it first. The trailing
marker was easy to miss; a file listing now shows
"⎇ scentbird-root/" rather than "scentbird-root/ ▣".
Noted as a one-liner in the code: per-entry colouring would need
dropping ratatui-explorer — out of scope here.
- Rename user-facing "repo" to "repository". The button label becomes
"Mount this repository" and the prompt title becomes
"Git repository detected". Identifiers (repo_dir, GIT_REPO_MARKER,
test fixture names) are left alone — this is a UI-string change only.
- Rename the middle git-prompt button from "Enter to pick subdirectory"
to "Pick a subdirectory" — imperative voice parallel to the first
button, no more "key + verb" mix. Footer hints under the prompt
still read "E enter" for the shortcut, which remains correct.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): uppercase single-letter hotkeys in footer hints
Operator prefers the `M mount · E enter · C/Esc cancel` style
consistently across the TUI. Previously the list-view footer was
lowercase (`e edit · n new · d delete · o open in GitHub · q quit`)
while the git-prompt hint was uppercase. Normalise every footer site
on uppercase single-letter keys; multi-character glyphs (Enter, Tab,
Esc, ↑↓, etc.) and non-alpha keys (`*`) pass through unchanged.
Updates:
- `src/launch/manager/render.rs` list footer: E/N/D/O/Q
- `src/launch/manager/render.rs` editor save footer: S
- `src/launch/manager/render.rs` contextual_row_items: D/A/O on
Mounts rows, A on the "+ Add mount" sentinel
- `src/launch/widgets/file_browser.rs` nav hint: S select,
H/← up
- Existing footer test assertions updated to match new casing
- New `footer_hotkeys_are_uppercase` test scans contextual hints
(Mounts row + sentinel, Agents) and verifies every single-char
alphabetic `Key` item is uppercase
Key handlers extended to accept both cases where a footer now shows
uppercase. Most handlers already matched `'e' | 'E'` from batch 11;
the remaining lowercase-only sites (list Q/E/N/D/O, list K/J nav,
editor S/K/J, editor-Mounts A/D/O, file_browser S/H/L nav, picker
J/K) now take `'x' | 'X'`. Behavioural change is nil — Caps Lock
and Shift-held hotkeys now work where they already did at the
footer-advertised case.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): click-to-select in workspace list
Extend `handle_mouse` with row-click selection on the workspace-list
pane. Left-button Down inside the list content area maps to the row
index and updates `ms.selected`; clicks on the seam column still
start a drag (regression guard for batch 14).
Hit-test rules:
- Seam always wins — a click within ±1 column of the current seam
starts a drag regardless of y. This keeps the resize affordance
unambiguous even when the seam overlaps a valid row position.
- Otherwise, clicks inside `[1, seam - 1]` × `[header + 1, body_end - 1]`
(left-pane interior minus borders) convert to a row index via
`mouse.row - (header_height + 1)`.
- The index must be in `[0, sentinel_idx]`; beyond that we silently
drop the click. Row 0 = "Current directory", 1..=N = saved, N+1 =
"+ New workspace" sentinel.
- Clicks outside those ranges (header, footer, borders, right pane)
are ignored.
Layout heights are pulled from two new private consts mirroring
`render::render`'s `Constraint::Length(3)`/`Length(2)`. If the
chrome ever changes shape, both the render and hit-test paths need
updating together.
Double-click = launch is intentionally skipped: crossterm doesn't
emit native double-click events, so implementing it would need
tracking `(last_row, last_instant)` on `ManagerState` with a debounce
window. That's more state-machine than one item in this batch
warrants — left as a follow-up. Single-click-to-select is the
must-have and is fully wired.
Five new tests cover the happy path (row 0, mid-list row, sentinel
row), the negative path (header / borders / right pane / below
sentinel / footer), and the seam precedence regression guard.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(launch): FileBrowser Esc steps back one level when not at root
Previously Esc always cancelled the modal. If the operator drilled into
a subfolder (via Enter on a plain dir or via the git-prompt
"Pick a subdirectory" path), a stray Esc collapsed the whole picker and
returned to the workspace list. Operators expect Esc to back out one
level — mirroring the behavior of `h` / `←` — and only cancel when
already at root.
Esc now:
- Clears any stale rejected_reason.
- Navigates one level up when cwd != root (sandbox-guarded, same as the
existing root-clamp).
- Cancels the modal only when cwd == root.
Git-prompt Esc is unchanged: it still dismisses only the prompt and
leaves the explorer open at the current cwd.
Footer hint updated from "Esc cancel" to "Esc up/cancel" (matching the
batch 16 uppercase-hotkey convention) — accurate for both drilled-in
and root cases.
Five new tests cover: esc-at-root cancels, esc-in-subfolder navigates
up, esc-three-levels-deep goes up exactly one, esc clears
rejected_reason, and the git-prompt Esc regression guard.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): default workspace list/details split 45 -> 30
Workspace names like `chainargos-blockchain-nodes` fit comfortably at
30%; the details pane gets the breathing room for git branches and
full paths. MIN/MAX bounds (20/80) stay unchanged.
Also refactor the seam drag-and-select tests in `input.rs` to refer to
`DEFAULT_SPLIT_PCT` instead of the literal `45`, so future changes to
the default don't silently break the test assumptions about the seam
column.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): agents subpanel moves default-marker star to trailing position
The read-only Agents sub-panel in the list/details pane previously placed
a leading star prefix on the default-agent row — "★ alpha" — so default
rows rendered at col 4 while non-default rows rendered at col 2. Move the
star to a trailing position after the name, separated by a space, so
every agent name starts at `SUBPANEL_CONTENT_INDENT` (col 2) matching
the General and Mounts sub-panels' leading-indent convention.
The star renders as its own `Span` styled with `PHOSPHOR_DIM` — keeps
the agent-name base color (`PHOSPHOR_GREEN` when registered globally,
`PHOSPHOR_DIM` when not) untouched and keeps the marker low-chrome.
The Editor tab's Agents view (`render_agents_tab`) is intentionally
untouched — that layout carries `[x]`/`[ ]` toggles with a different
selection affordance and is a separate concern.
Replaces the legacy `agent_star_row_aligns_with_content_column` test
with three focused tests covering name alignment on non-default rows,
trailing-star position on default rows, and name alignment on default
rows (independent of the trailing star).
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): show origin URL in FileBrowser git-repo prompt
Enter on a git-repo folder now opens the "Git repository detected"
overlay with the origin web URL displayed between the title and the
question. The URL is resolved via `mount_info::inspect` when the
prompt opens and cleared when the prompt is dismissed, so stale URLs
don't leak between repos.
Non-GitHub remotes (and repos with no origin) resolve to `None` and
the URL row is elided entirely — no "(no URL)" noise. The overlay
height grows by one row only when a URL is present.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(launch): Esc rewinds the create-workspace wizard one step
Esc at any step of the create-workspace prelude now steps back to
the previous step instead of abandoning the whole wizard:
- FileBrowserSrc (step 1) → workspace list (no prior state)
- MountDstChoice → reopen FileBrowserSrc at the last cwd
- TextInputDst → reopen MountDstChoice
- WorkdirPick → reopen MountDstChoice or TextInputDst
depending on which dst-path was taken
- TextInputName → reopen WorkdirPick
`CreatePreludeState` grows two breadcrumb fields (`last_browser_cwd`,
`used_edit_dst`). `FileBrowserState` exposes `cwd()` / `set_cwd()`
so the wizard can restore the exact directory the operator was
browsing when src was committed — no more starting back at $HOME.
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(launch): replace ratatui-explorer with a custom file browser
ratatui-explorer 0.3's Theme exposes only a single `dir_style` shared
by every directory row — meaning the operator affordance "highlight
git repos differently" was impossible without a rewrite. The custom
browser renders each row directly via tui-widget-list + Paragraph, so
git rows can carry a distinct trailing ` (git)` suffix in phosphor-dim
bold while plain folders stay plain phosphor green.
Other gains vs the ratatui-explorer wrapper:
- `h/l`, arrows, `s`, `Esc`, and Enter are handled directly — no more
round-tripping through `FileExplorer::handle` with div-by-zero guards
on empty listings.
- `cwd()` / `set_cwd()` are first-class methods, used by the create-
workspace wizard's step-back navigation.
- `FolderEntry { is_git }` replaces the fragile U+2387 prefix hack.
- The EXCLUDED-at-root / sandbox / $HOME-clamp / `~/.jackin` rejection
behaviour is preserve…
donbeave
added a commit
that referenced
this pull request
May 7, 2026
* docs(specs): workspace manager TUI (PR 2 of 3) Design spec for PR 2 of the launcher-workspace-manager series. Adds an interactive workspace manager screen to the jackin launcher — list, create, edit, and delete workspaces without dropping to CLI. Reached via `m` from the existing Workspace picker; Esc returns to the launcher. Launch path stays keystroke-identical. Key design decisions from brainstorming (all settled, no open questions): - Entry model: separate Manager screen on `m` keypress; launch path unchanged. - Editor tab set: General · Mounts · Agents · Secrets-stub. Secrets placeholder locks in the final tab strip so PR 3 fills in the panel without a visual reshuffle. - Text-edit UX: modal push — centered overlay, one reusable TextInput widget. - Staging: explicit save via `s`. Pending changes drive dirty markers; Esc with pending opens Discard/Save/Cancel. - Create flow: mounts-first wizard — file browser for host source, dst auto-defaulted to the same absolute path as src (host-path mirror), workdir picked from mount dsts + ancestors (never free-text), name last with live uniqueness check. - Delete UX: single-line Y/N confirm modal. - Style: reuses jackin's existing digital_rain (src/tui/animation.rs), step_shimmer, spin_wait, and landing-page color tokens from docs/src/components/landing/styles.css. One new area-bounded rain widget extracted from animation.rs. Three new reusable widgets emerge (TextInput, FileBrowser, Confirm) that PR 3's Secrets tab will consume unchanged. All persisted writes flow through ConfigEditor (established in PR 1, merged in #162). Non-goals: per-(workspace × agent) env overrides (PR 3), global mount management (CLI only), agent lifecycle from manager (CLI only), CLI surface changes, CHANGELOG. * docs(specs): lock third-party widget choices for PR 2 Amends the workspace manager TUI spec with a Third-party dependencies subsection that names the three ratatui ecosystem crates we'll adopt: - ratatui-textarea (v0.9.x) — single-line TextInput (ratatui-org owned) - ratatui-explorer (v0.3.x) — FileBrowser with folders-only wrapper - tui-widget-list (v0.15.x) — WorkdirPick list mechanics All three require the ratatui unstable-widget-ref feature flag. Rejected with rationale so reviewers don't re-litigate: tui-input (superseded by ratatui-textarea), tui-confirm-dialog / tui-overlay (Confirm modal is cheaper hand-rolled), rat-widget (too opinionated), throbber-widgets-tui / ratatui-cheese (we have spin_wait already), ratatui-toaster (banner is ~30 LOC with step_shimmer), tui-logger (jackin has no log or tracing framework today). Also updates Rollout section — "no new dependencies" was no longer accurate. --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com>
donbeave
added a commit
that referenced
this pull request
May 7, 2026
* docs(plans): workspace manager TUI implementation plan
Twenty-two-task TDD-shaped implementation plan for the workspace
manager TUI specced in #164. Ordered in seven phases:
1. Foundation — deps + module scaffolds + animation.rs refactor
2. Widgets — Confirm, TextInput, FileBrowser, WorkdirPick, PanelRain
3. State machine — ManagerState, EditorState, CreatePreludeState
4. Render — list view, editor (4 tabs), modal dispatcher
5. Input — modal-first key dispatch with per-stage routing
6. Integration — LaunchStage::Manager wire-in, m keybinding, full
editor + create key handling, ConfigEditor save/create/delete paths
7. Polish — style effects (boot reveal, save shimmer, toast expire),
integration test, final verification + PR
Each task is TDD-shaped: write failing test → run fails → implement →
run passes → commit. Complete code in every implementation step. No
placeholders.
Scope cut documented in self-review: tab-slider and panel-focus-glow
animations from the spec's Style section are omitted; they're cosmetic
and can land in a follow-up PR without rework.
* feat(launch): scaffold workspace manager modules + widget deps
Adds three ratatui ecosystem crates (ratatui-textarea 0.9,
ratatui-explorer 0.3, tui-widget-list 0.15) and enables ratatui's
unstable-widget-ref feature. Creates empty module structures at
src/launch/widgets/ and src/launch/manager/ to land typed setters,
widgets, and state transitions in subsequent commits.
No behavior change.
* refactor(tui): extract render_rain_frame for reuse
Separates the per-frame rain rendering from digital_rain's event loop
so the upcoming PanelRain widget can render bounded-area rain without
duplicating the renderer. tick_rain and RainState become pub(crate)
for the same reason. Fullscreen digital_rain is rewritten to delegate
to render_rain_frame. No visible change.
* feat(launch): Confirm widget — Y/N modal
Hand-rolled Y/N confirmation dialog. Case-insensitive, Esc cancels.
~60 LOC + 5 tests. Used by delete-workspace and discard-changes flows.
* feat(launch): TextInput widget — single-line via ratatui-textarea
Wraps TextArea in single-line mode (intercepts Enter and Ctrl+M so
newlines are never inserted). Exposes a ModalOutcome<String> contract:
Enter commits, Esc cancels, everything else passes through to the
textarea for cursor / insert / backspace handling.
Cursor is placed at end of initial text on construction so editing
feels natural (backspace works immediately on prefilled values).
* feat(launch): FileBrowser widget — wraps ratatui-explorer
Folders-only filter, seeded from $HOME by default, adds 's' as
select-current-folder. Delegates all navigation (h/l/j/k/Enter/
Backspace/Home/End/PgUp/PgDn/Ctrl+h) to ratatui-explorer defaults.
* feat(launch): WorkdirPick widget — choice list via tui-widget-list
Derives the pick list from mount dsts + each ancestor up to /, with
labels (mount dst / parent / root). Deduplicates when multiple mounts
share ancestors. Enter commits selected path, Esc cancels.
* feat(launch): PanelRain widget — area-bounded phosphor rain
Wraps tui::animation's RainState engine for rendering into a bounded
Rect. Tick + render are separate so callers control frame rate.
Resizes state when the rect changes shape.
Adds RainState::new(cols, rows) constructor to animation.rs so the
widget can initialize state without duplicating the column/grid setup
that was previously inlined in digital_rain().
* feat(launch): ManagerState + EditorState + CreatePreludeState types
Defines the top-level state machine per spec § 3: ManagerStage enum
(List / Editor / CreatePrelude / ConfirmDelete), EditorState with
dirty detection and change_count, CreatePreludeState with the
mounts-first wizard step enum, Modal enum with target enums, Toast
type, and constructors. Tests cover WorkspaceSummary derivation,
ManagerState::from_config, EditorState dirty detection, and
CreatePreludeState initial step.
ManagerStage and ManagerState carry a lifetime parameter propagated
from TextInputState<'a> (ratatui-textarea borrow). MountConfig lacks
Ord/Hash so change_count uses linear containment checks rather than
BTreeSet symmetric_difference.
Transitions and key handling are filled in by subsequent tasks.
* feat(launch): create-workspace wizard state transitions
Mounts-first flow: PickFirstMountSrc → PickFirstMountDst → PickWorkdir →
NameWorkspace. Each accept_* method advances the step. default_mount_dst
mirrors the host src path. default_name derives from the dst basename.
build_workspace assembles the final WorkspaceConfig.
* feat(launch): manager list view render
Renders ManagerStage::List: header banner, horizontal-split body
(workspace list + details pane), footer hint. Other stages rendered
by subsequent tasks (12: editor, 13: modal dispatcher).
* feat(launch): editor view render (all four tabs)
Renders Editor stage with General / Mounts / Agents / Secrets-stub
tabs, dirty markers on changed fields, save-count footer. Error
banner overlays the top of the tab body using --landing-danger
(#ff5e7a) for real errors.
Refactors top-level render to let stages declare whether they use
shared chrome (List, future ConfirmDelete) or their own full-screen
layout (Editor, future CreatePrelude).
* feat(launch): modal render dispatcher
Centers a modal Rect at 60x30 percent of the frame and dispatches to
the appropriate widget's render function based on the Modal variant.
* feat(launch): key dispatcher with modal precedence
Scaffolds handle_key with modal-first precedence: if a modal is open
anywhere in the state machine, events route to the modal handler
before per-stage handlers. Full editor + prelude wiring lands in
Tasks 16 / 17; this commit has stubs for those to keep the compiler
happy. List and ConfirmDelete stages are fully wired (navigation,
delete flow via ConfigEditor).
* feat(launch): LaunchStage::Manager + m keybinding
Adds a third launch stage and wires an m keypress from the Workspace
picker to transition into it. run_launch now takes AppConfig by value
+ &JackinPaths so the manager can open ConfigEditor. Footer hint in
the Workspace stage gains 'm manage'.
* feat(launch): editor key handling — tabs, save, discard, field edits
Implements Tab/Shift-Tab navigation between tabs, ↑↓ row selection,
Enter-to-edit (opens modal per field type), Space/* on Agents tab,
a/d on Mounts tab. s triggers save via ConfigEditor::edit_workspace
or create_workspace, with error banner on failure. Esc with pending
changes opens the Discard confirm modal; Esc with clean state returns
to the manager list.
* feat(launch): full create-workspace prelude flow
Chains the four modals (file browser → dst TextInput → workdir pick →
name TextInput) through CreatePreludeState. On completion, transitions
to Editor(mode=Create) with everything pre-populated. s in the editor
creates via ConfigEditor::create_workspace.
* feat(launch): manager style effects
Boot reveal on manager entry via tui::animation::digital_rain(400, None).
Save toast auto-expires after 3s. Shimmer: toast text flashes white during
the first 400ms post-show. JACKIN_NO_ANIMATIONS=1 disables the rain
transition.
* test(launch): end-to-end manager delete-workspace flow
Drives manager::handle_key with scripted key events (d, y). Asserts
the workspace is removed from on-disk config, the manager transitions
back to List, and the in-memory workspace list refreshes. Regression
guard against state-machine drift.
* style(launch): quiet clippy / fmt for workspace manager
Fix all clippy warnings introduced by the workspace-manager TUI (~1500
lines of new code): unnested or-patterns, collapsible-if, elidable
lifetimes, default-trait-access, items-after-statements, match-for-
equality, match-for-single-pattern, needless-pass-by-ref-mut,
doc-markdown (missing backticks), missing-const-for-fn, uninlined-
format-args, manual-Debug-non-exhaustive, large-enum-variant (allow),
too-many-lines (allow), unnecessary-trailing-comma, and the associated
fmt diff (11 files reformatted).
No logic changes; all tests pass (workspace_config_crud requires
--test-threads=1 due to pre-existing set_current_dir race).
* feat(launch): replace Workspace picker with manager as initial stage
The old Workspace picker stage is removed. LaunchStage::Manager becomes
the initial stage — jackin opens directly to the manager. Enter on a
workspace launches it (via the existing Agent picker); e opens the
editor; n creates; d deletes; q/Esc exits jackin. The m keybind is gone
— nothing to enter since we are already in the manager.
Esc from the Agent picker returns to the manager list (was: Workspace
picker, which no longer exists).
Also removes the mid-loop digital_rain(400, None) boot reveal that was
fighting with skippable_sleep's raw-mode toggling, which caused arrow
keys to print as raw escape sequences in the manager instead of being
captured by crossterm events.
* fix(launch): render active modals in manager
render_modal was defined but never called. Modals in Editor and
CreatePrelude stages transitioned correctly in state but had no
visible effect on screen — pressing n would silently put the user in
the create wizard with an invisible FileBrowser, making the create
and edit-field flows appear broken.
Also renders the ConfirmDelete variant's confirm modal directly
(ConfirmState on ConfirmDelete is a top-level field, not wrapped in
Modal::Confirm).
* fix(launch): modal footer hints + stage-aware manager footer
File browser and text input modals were rendering without any hint
about the keys to commit/cancel. Users opening the create workspace
flow saw the folder picker but couldn't progress — pressing Enter
only descended into folders (s is the select key for ratatui-explorer),
and there was no visual cue about the right key.
Adds a one-line phosphor-dim italic footer inside each modal:
- FileBrowser: ↑↓ navigate · Enter open · h/← up · s select · Esc cancel
- TextInput: Enter confirm · Esc cancel
Also makes the top-level manager footer hint stage-aware:
- List stage: existing navigation hint (unchanged)
- CreatePrelude: Create workspace · follow the prompts · Esc cancel
- ConfirmDelete: Y yes · N no · Esc cancel
- Editor: still delegates to render_editor's own footer
* fix(launch): Esc in create wizard returns to list immediately
Previously pressing Esc inside the first create-wizard modal cleared
the modal but left the state machine stuck in ManagerStage::CreatePrelude
with no modal active — render drew a blank body, requiring a second Esc
to reach the non-modal prelude handler that transitions back to List.
Now the post-modal check distinguishes three outcomes: in-progress
(modal still open), complete (wizard finished with name), and
cancelled (modal cleared without a name). Cancelled transitions to
List in the same input pass.
* feat(launch): wire rename + render all agents on Agents tab
Fixes two spec gaps:
1. Agents tab rendered only currently-allowed agents, so the user
couldn't add agents via Space toggle — non-allowed agents were
invisible. Now iterates config.agents (the full set) and shows
[x] or [ ] per agent based on pending.allowed_agents membership.
Threads &AppConfig through manager::render → render_editor →
render_agents_tab. Also fixes set_default_agent_at_cursor to use
config.agents for cursor-to-agent resolution instead of the
allowed-only list.
2. Workspace rename was a TODO. Now:
- ConfigEditor gains rename_workspace(old, new) using toml_edit's
key-rename (preserves nested tables + array-of-tables). Rejects
empty new name, collision, and missing old name.
- General tab's name row is editable on Enter (in Edit mode) via
TextInput modal.
- apply_text_input_to_pending stashes the name on
EditorState::pending_name.
- save_editor calls rename_workspace before edit_workspace when
pending_name differs, then updates editor.mode so subsequent saves
target the new name.
- change_count + is_dirty + render dirty marker all track the rename.
Tests: three new unit tests on ConfigEditor::rename_workspace covering
happy path (nested tables preserved), collision rejection, and empty-
name rejection.
* fix(launch): FileBrowser s commits cwd, not highlighted entry
Pressing s in an empty or file-only folder previously committed the
highlighted entry, which in such a folder is '../' — so the user got
the parent directory, not the folder they were viewing. This was
especially bad for newly-created empty workspace source folders.
Now s commits the explorer's current working directory via
FileExplorer::cwd() (ratatui-explorer 0.3.x). User intent is preserved:
'I've navigated to this folder — select it.' Footer hint updated from
's select' to 's use this folder' to reflect the semantics.
* fix(launch): render active-row cursor in editor tabs
EditorState::active_field tracked the cursor but render functions
didn't display it — users couldn't tell which row Enter / Space / * /
a / d would target. Add a ▸ prefix and phosphor-green bold to the
selected row across all three tabs (General, Mounts, Agents).
Also clamp Down-arrow to the last valid row so the cursor can't run
off the end of the visible content, and thread &AppConfig through to
handle_editor_key's Down handler so it can size the Agents tab's
row count correctly.
* fix(launch): UX polish pass on manager create/edit flows
Nine polish fixes reported after live walkthrough:
1. TextInput/Confirm modals were 30%-of-screen tall, rendered as big
empty boxes around a single line. Now variant-aware: inputs/confirms
are 5-6 rows fixed; file browser and workdir pick stay taller for
their scrolling lists.
2. 'last used' row hidden in Create mode (no history exists).
3. 'default agent' row hidden in Create mode (no agents picked yet).
4. Footer hint is now row-contextual: 'Enter rename' on name row,
'Enter pick workdir' on workdir row, 'a add / d remove' on mounts,
'Space toggle / * set default' on agents, nothing on read-only
rows. Base hint says 's save workspace' (was 's save') for clarity.
5. File browser gets a prominent outer block titled '<cwd> · press
[S] to use this folder' — the select affordance was previously
buried in a dim footer line.
6. Mount rows collapse 'src → dst' to just 'path' when src == dst
(host-path-mirror default — redundant arrow gone).
7. Mounts tab '+ Add / − Remove selected' footer uses white-bold for
the action words to distinguish from the mount list.
8. Agents tab gets a top banner clarifying empty = 'all allowed'
semantics vs non-empty = custom allow-list.
9. Read-only rows (last used) no longer advertise Enter in the footer.
Also fix max_row_for_tab in input.rs: Create mode General tab only
has 2 rows (name read-only + workdir), not 4.
* fix(launch): Confirm modal shows Y/N as styled buttons
Previously the modal rendered '[Y]es · [N]o (default) · Esc cancel' as
inline text inside a tall 30%-of-screen box, which looked like a
multi-line textarea. Now:
- Modal is compact (6 rows)
- Yes/No render as inverted-video buttons, centered
- No (default) uses white-on-black to distinguish as default action
- Esc cancel moves to a dim italic footer hint at the bottom
- Prompt text stays bold-white at the top
Enter intentionally unbound — destructive confirms should not commit
on accidental Enter presses.
* fix(launch): remove nested borders in FileBrowser modal
ratatui-explorer's widget renders its own bordered block with the CWD
as title. The prior polish pass added an outer block with 'press [S]
to use this folder' — the result was double borders, ugly and
confusing.
Drop the outer block. Show the 'press [S]' affordance as a bold-white
centered line ABOVE the explorer (no border), and keep the dim
navigation hint as a line BELOW. The explorer's own cwd-titled block
stays — no nesting.
* feat(launch): Confirm modal gains Tab focus + Enter commits focused
Adds standard confirmation-dialog UX: Tab / Shift+Tab / ←→ / h/l cycle
focus between Yes and No; Enter commits the focused button. Default
focus is No (destructive action protection — accidental Enter won't
commit Yes). Y/N direct shortcuts still work regardless of focus.
Visual: focused button gets white bg + black text + bold; unfocused
gets phosphor-green bg. Footer hint updated to mention Tab + Enter.
Modal grows from 6 to 7 rows for a second spacer between buttons
and hint (was visually cramped).
* fix(launch): shrink FileBrowser + WorkdirPick modal heights
Prior sizing was 60 rows for FileBrowser and 40 rows for WorkdirPick —
effectively fullscreen on a typical 40-50 row terminal. Tight 20 and
12 rows fit comfortably and still show enough entries without the
modal swallowing the whole screen.
* feat(launch): + Add mount is a selectable sentinel row
Mirrors the + New workspace sentinel in the manager list. The Mounts
tab now renders + Add mount as a real selectable row at the end of
the list, selected via ↑↓, activated via Enter. Visual treatment is
white bold (distinguishing it from the green mount rows).
- max_row_for_tab reports len() (mount count + sentinel index) for
Mounts so ↓ can reach the sentinel.
- remove_mount_at_cursor is a no-op on the sentinel (guard already existed).
- a (anywhere on the tab) still works as a quick-add shortcut.
- Contextual footer hint differentiates between 'on a mount row'
(d remove · a add) and 'on the sentinel' (Enter add · a add).
* feat(launch): Agents tab shows [all] / [custom] status badge
Replaces the implicit 'empty list = all allowed' with an explicit
status line at the top of the Agents tab:
Allowed agents: [ all ] (when allowed_agents is empty)
Allowed agents: [ custom ] (3 of 5 allowed) (when non-empty)
The badge is an inverted-video token (phosphor-green bg for 'all',
white bg for 'custom') making the current mode immediately visible.
The agent list below stays as a checklist — toggling updates the
status badge live.
Cursor semantics also shift: cursor is now 0-based into config.agents
(no more header-offset-by-one). toggle_agent_allowed_at_cursor and
set_default_agent_at_cursor are updated accordingly. max_row_for_tab's
Agents arm drops to len()-1.
set_default_agent_at_cursor now also auto-allows the agent being set
as default (was previously a no-op if the agent wasn't already in
allowed_agents).
* feat(launch): Mounts tab shows folder / git · <branch>
Adds a mount_info helper that inspects the host-side src path on
render: checks for .git as dir or submodule-gitfile, reads HEAD, and
reports the current branch (or detached short-sha). Renders next to
each mount row as dim italic metadata:
/Users/…/repo (rw) · git · main
/Users/…/scratch (rw) · folder
/Users/…/gone (rw) · missing
Six unit tests cover: missing path, plain folder, normal repo with
branch, detached HEAD, submodule .git file, label formatting.
* feat(launch): Save/Discard/Cancel modal + richer details pane
Two UX upgrades:
1. Exit-with-changes now offers three explicit choices instead of
binary 'Discard Y/N'. New SaveDiscardCancel modal with three
buttons (Save / Discard / Cancel), Tab cycles focus, Enter commits
the focused option. S/D/C/Esc shortcuts work regardless of focus.
Default focus is Cancel (safest). Save intent triggers ConfigEditor
save → list; Discard just drops pending; Cancel keeps the editor.
2. Manager list's details pane now shows the full mount list (with
folder / git · <branch> labels, same as the Mounts tab) and the
allowed-agents list (or 'any agent' when unrestricted). Title drops
the duplicate workspace name since the list selection already shows
it.
5 new unit tests on SaveDiscardState covering focus cycling and key
shortcuts.
* fix(launch): restrict FileBrowser to \$HOME, rename main title
Four UX fixes:
1. Main manager screen title 'manage workspaces' → 'workspaces'
(the screen does more than manage — launch, create, edit, delete).
2. FileBrowser modal goes fullscreen (100% x 100%) so the main chrome
doesn't peek through and confuse the visual.
3. FileBrowser now:
- Starts at \$HOME (already did)
- Excludes Library, Applications, Movies, Music, OrbStack, Pictures
from the listing via filter_map
- Clamps cwd back to \$HOME if the user escapes above it via
set_cwd() (ratatui-explorer 0.3.x has this method)
- Rejects \$HOME itself as a workspace source
- Rejects ~/.jackin/* (jackin's reserved data area)
4. Rejected selections show an inline red error banner
(#ff5e7a) above the explorer. Cleared on next keypress.
* fix(launch): display paths as ~/… via shorten_home
Paths starting with $HOME now render as '~/...' in the TUI:
General tab workdir, Mounts tab rows, details pane mounts/workdir,
WorkdirPick choices. Consistent with jackin's existing shorten_home
helper (already used elsewhere in the launcher).
Paths stored on disk are unchanged — this is display-only.
* fix(launch): mount table formatting + FileBrowser resize/colors
Two fixes:
1. Mount lists in the details pane and Mounts tab render as an
aligned 3-column table (path, mode, type) instead of a free-form
line where the '(rw)' tag and type metadata floated at variable
positions. shorten_home applied to paths consistently via the
shared format_mount_rows helper, which is called from both
render_details_pane and render_mounts_tab.
2. FileBrowser modal goes from fullscreen (100%) to 70%x70%, letting
the surrounding chrome show again so the dialog reads like a
dialog, not a whole screen. Theme configured to use jackin's
phosphor palette (green text, bright-phosphor highlight, shortened
CWD title via shorten_home in a dynamic with_title_top closure).
* fix(launch): drop Agents 'default' column + cap workspace list height
1. Agents tab header 'allowed? · default · agent' → 'allowed? · agent'.
The star marker next to the agent name already indicates default;
the dedicated column was empty for every non-default row.
2. Manager list body now caps at content height (workspace count + 2
border rows + 1 sentinel row) instead of filling the whole frame.
5-6 workspaces no longer render in a box that looks two-thirds
empty.
* Revert "fix(launch): cap workspace list height to content"
The height cap made the space below the boxes visibly empty, which
reads worse than the previous full-height boxes. User feedback:
'before it was better when it was using the whole vertical space.'
Keeps the Agents tab header change from the same original commit
(3fdab9f3) — only the list-body sizing is reverted.
* fix(launch): hide FileBrowser .. entry at $HOME root
Previously the '../' entry was always shown in the file browser.
When the user was at $HOME, selecting it would escape the sandbox
(and was then clamped back by set_cwd) — confusing and cluttered.
Now the filter hides '..' when its target path is outside the root
subtree. At $HOME the entry disappears; at any subfolder of $HOME
it still appears so the user can navigate back up.
* feat(launch): hide empty right pane, split details, clickable git links
Three UX improvements:
1. When the cursor is on '+ New workspace' in the manager list,
the right details pane is hidden entirely — the list takes full
width. No more empty bordered box for the sentinel row.
2. Details pane split into three stacked sub-panels: General (workdir
+ last used), Mounts (tabular with header row), Agents (list or
'any agent'). Each has its own bordered mini-block with phosphor-
dark border and white-bold title. The outer 'Details' block is gone.
3. Git branch URL resolution wired up: inspect() now parses
<git_dir>/config to find the origin remote and derives a web URL
(GitHub, GitLab, generic HTTPS/SSH). MountKind::Git gains a
web_url: Option<String> field; MountKind::labeled_hyperlink() wraps
the branch name in OSC 8 escape sequences for supported terminals
(iTerm2, kitty, WezTerm, Alacritty, modern Terminal.app).
OSC 8 fallback: ratatui's Paragraph widget strips raw ESC bytes, so
the render path continues to call label() (plain text). The
hyperlink infrastructure (labeled_hyperlink, osc8_link, web_url) is
retained for a future raw-terminal-write path. Both are annotated
#[allow(dead_code)] with an explanatory TODO.
5 new unit tests on remote-URL parsing (GitHub SSH, GitHub HTTPS,
ssh:// protocol, GitLab SSH, config-file parse). All 566 tests pass.
* fix(launch): polish FileBrowser, WorkdirPick, and mount-dst prompt
- FileBrowser entries now render white instead of phosphor-green so the
bright-green highlight is the unambiguous focus indicator.
- TextInput prompts for mount destination say "destination (default:
same as host path)" instead of the internal "Mount dst" phrasing.
- WorkdirPick lines are laid out as a table: the path column is padded
to the widest choice so the dim+italic label column (`(mount dst)`,
`(parent)`, `(root)`, `(home)`) lines up cleanly.
- WorkdirPick filters `/` and the literal parent of `$HOME` (e.g.
`/Users` on macOS, `/home` on Linux) from the choice list — those
paths are never useful workdir targets.
- When a path is exactly `$HOME`, label it `(home)` instead of
`(parent)` so the workspace operator sees a recognisable name.
* fix(launch): FileBrowser s commits highlighted folder
Previously `s` always committed the explorer's cwd, which meant the
operator had to press Enter to navigate into the target folder before
committing — even though the folder was already highlighted and the
target of a single Enter press.
Reading `FileExplorer::current()` lets us commit the highlighted entry
directly when it is a real child directory. The synthetic `../`
parent-link row and the empty-listing case both fall back to the cwd,
preserving the previous behaviour for those edge cases.
The existing $HOME and `~/.jackin/*` rejection rules apply to whichever
path is chosen as the commit target.
* fix(launch): keep General-tab labels white; note agent-hyperlink TODO
- render_editor_row and render_editor_readonly_row no longer shift the
label column to phosphor-green when the row is focused. Labels stay
white (bold when focused); values keep their phosphor colouring for
editable rows and dim phosphor for read-only rows.
- Read-only rows used to render everything in phosphor-dim, which made
the editor view look washed-out. They now match the editable-row
label treatment (white) with a dim value + italic "(read-only)"
suffix, giving the operator a cleaner signal-to-noise ratio.
- Added a TODO in render_agents_subpanel mirroring the existing
labeled_hyperlink() note in render_mounts_subpanel: ratatui's
Paragraph strips OSC 8 ESC sequences, so agent-name → GitHub links
stay plain-text until a raw-write path exists.
* feat(launch): gate editor save on mount-collapse plan
The editor used to write configs straight to disk via
`ConfigEditor::edit_workspace` (or `create_workspace`), which meant the
operator could save a workspace with overlapping mounts like
`~/Projects` and `~/Projects/test`. The CLI rejects this unless you
confirm or pass `--prune`; the TUI now does the same.
Flow:
- On `s`, run `workspace::planner::plan_edit` (Edit) or `plan_create`
(Create) against the pending mount set.
- `CollapseError::{ReadonlyMismatch, ChildUnderExistingParent}` ->
error banner, no write.
- Pre-existing collapses only (no edit-driven) -> error banner
referencing `jackin workspace prune <name>`. The operator can't fix
these from the editor alone and the CLI prune command already exists
for this case.
- Edit-driven collapses -> open a `Modal::Confirm` with a
`ConfirmTarget::SaveCollapse` target, listing each child/parent pair
in the same wording as the CLI. On Yes, the save re-enters with
`EditorState::collapse_approved = true` and commits the collapsed
mount set via `plan.effective_removals` / `plan.final_mounts`. On No
/ Esc, pending mounts are kept intact so the operator can edit by
hand.
Pattern: a boolean flag on `EditorState` + a new `ExitIntent::RetrySave`
variant so the confirm-yes path reuses the existing modal-exit routing
but stays in the editor on success (rather than bouncing to the
workspace list, which is what `ExitIntent::Save` does). The plan
itself is not stashed; it is cheap to recompute on re-entry.
The `Confirm` widget now grows its prompt region to match the number
of lines in `state.prompt`, and `render_modal` sizes the outer rect
via `confirm::required_height` so multi-line collapse summaries render
without clipping.
Tests (5 new):
- `save_editor_opens_confirm_on_edit_driven_collapse`
- `confirming_collapse_writes_collapsed_set`
- `cancelling_collapse_keeps_pending_mounts_intact`
- `readonly_mismatch_produces_error_banner_no_write`
- `pre_existing_collapse_produces_prune_error_banner`
* feat(launch): structured footer with per-item styling
Introduce a `FooterItem` enum (Key / Text / Dyn / Sep / GroupSep) and a
shared `render_footer` that emits spans with a consistent palette:
- Key glyphs (↑↓, Enter, e/n/d/q, Tab, Esc, S, Y/N, *, Space) render in
WHITE + BOLD so they pop out of the legend.
- Action labels ("launch", "edit", "new", …) render in PHOSPHOR_GREEN.
- Inline dots (·) render in PHOSPHOR_DARK as a faint separator.
- A GroupSep (three spaces, no style) introduces a wider visual gap
between logical groups — navigation, per-row actions, and exit.
Migrate every footer call site to this scheme:
- `manager/render.rs` List / CreatePrelude / ConfirmDelete / Editor
footers build `Vec<FooterItem>` explicitly so the grouping is
deliberate per stage.
- Agent-screen footer in `launch/render.rs` uses the same inline spans.
- Modal-local hints inherit the scheme (file_browser navigation + "[S]
to use this folder" affordance, text_input "Enter confirm · Esc
cancel", confirm "Tab cycle · Enter confirm · Y yes · N no", and
save_discard).
Add unit tests covering the span-style mapping per variant plus
smoke tests for the List and ConfirmDelete stage footers.
* fix(launch): keep right pane visible on '+ New workspace' sentinel
Batch 7 expanded the list to full width when the cursor landed on the
sentinel row. The operator wants the 45/55 split preserved — the layout
should not shift as the cursor moves — with the right pane rendered as
an empty bordered block (same PHOSPHOR_DARK border as the General /
Mounts / Agents sub-panels) when there is no workspace to describe.
* fix(launch): align mount-table header with data columns
The mount-table header was a hardcoded string (" path<23 spaces>mode<3
spaces>type") while data rows computed their path column width
dynamically from the widest row. When paths were shorter than 23 chars
the header appeared drifted relative to the data; when they were longer
the header's "mode" column collided with the data's mode column at a
different offset.
Share the column-width computation between the header and data rows:
- Extract `mount_path_width` which returns max(row_path, "path".len(),
10) so the header and data always use the same column boundary.
- Add `render_mount_header(path_w)` that uses the same format string as
the data rows, then have both the read-only details subpanel and the
editor Mounts tab consume it.
- Pin the `mode` column to a shared `MOUNT_MODE_COL_WIDTH = 4` constant
(covering "mode" as well as "rw"/"ro" + trailing space) so it no
longer over-pads inconsistently.
Add unit tests that build mount rows with mixed path lengths and assert
the header's "mode" column starts at the same character index as each
data row's "mode" column.
* feat(launch): describe workspace concept on '+ New workspace' pane
Replace the empty bordered block shown to the right of the manager list
when the sentinel row is focused with a two-panel description pulled
from the "What is a workspace?" / "Why save a workspace?" sections of
the workspaces guide. Keeps the right-hand real estate useful for
first-time operators and matches the General/Mounts/Agents sub-panel
chrome for visual consistency.
* feat(launch): restore 'Current directory' row in workspace manager
Before the TUI redesign the launcher's first row was a synthetic
"Current directory" choice that let operators launch an agent against
cwd without saving a workspace. The manager's rewrite dropped it; this
reinstates it as row 0 of the list with the right-pane summary, the
cwd-aware preselect, and the launch wiring that matches the old
behaviour.
Row layout (enforced by ManagerState::from_config, render_list_body,
and handle_list_key):
row 0 → synthetic "Current directory"
rows 1..=N → saved workspaces
row N+1 → "+ New workspace" sentinel
Edit (`e`) and Delete (`d`) are rejected on row 0 with a toast. Enter
on row 0 emits a new InputOutcome::LaunchCurrentDir; the run-loop
routes it through the same agent-picker transition as LaunchNamed,
reusing LaunchState::workspaces[0] (the CurrentDir choice built by
LaunchState::new). Preselect reuses find_saved_workspace_for_cwd so
TUI and CLI agree on "which workspace am I in?".
The right pane branches on row 0 → render_current_dir_details_pane
(dedicated renderer; no last-used row, no edit affordance, "any
agent"). The sentinel description pane lands in the same commit's
sibling already; saved-workspace rows continue to use the shared
render_details_pane with `workspaces[selected - 1]`.
Tests added:
- manager_preselects_saved_workspace_matching_cwd
- manager_preselects_current_directory_when_no_saved_matches
- manager_current_directory_is_first_row
- current_directory_row_rejects_edit_and_delete
- enter_on_current_directory_returns_launch_current_dir
* fix(launch): polish mount-header gap, modal titles, and FileBrowser size
- Mount table header: add two-space gutter between `mode` and `type`
so the header no longer reads "modetype". Data rows now emit the
matching two-space gap so the `type` column aligns in both the
read-only Mounts subpanel and the editor Mounts tab.
- Text-input + Workdir-pick modal block titles render WHITE + BOLD to
match the General/Mounts/Agents block titles on the main screen.
Confirm + SaveDiscard already use WHITE+BOLD — left untouched.
- WorkdirPick path values render WHITE (the `(mount dst)`/`(parent)`/
`(home)`/`(root)` label suffix stays PHOSPHOR_DIM italic).
- FileBrowser modal height drops from 70 absolute rows to 22 so it
no longer eats the whole screen. Width stays at 70%.
* feat(launch): classify git mounts by host and relabel GitHub remotes
- Introduce `GitHost { Github, Other }` on `MountKind::Git` so the
render path can tell which remotes have an "open in browser"
affordance. `inspect` populates this from `parse_remote_origin_url`:
SSH `git@github.com:`, HTTPS `https://github.com/…`, and
`ssh://git@github.com/…` all resolve to `Github`; anything else
(self-hosted, GitLab, no remote, unparseable URL) falls through to
`Other`.
- `remote_to_web` now returns `Some(url)` only for GitHub hosts and
`None` for everything else — it no longer synthesises `gitlab.com`
URLs. Non-GitHub remotes keep `web_url: None` on the `MountKind`.
- `MountKind::label()` renders `github · {b}` / `github · detached {sha}`
/ `github` for GitHub hosts and keeps the generic `git · …` prefix
for `Other`. `MountKind::Folder` / `Missing` unchanged.
- `remote_to_web_gitlab` test re-purposed to assert GitLab (and other
non-GitHub hosts) now return `None`. New tests for the GitHost split
via `inspect` and for the `remote_points_at_github` predicate covering
all three URL forms + a GitHub-lookalike subdomain rejection.
* feat(launch): 'o' key opens highlighted GitHub mount in the browser
- Add the `open` crate (5.x) so the editor can launch the system
browser without blocking the TUI (`open::that_detached`).
- Wire `o` into the editor's Mounts tab: when the cursor is on a
mount row whose source resolves to a GitHub-hosted repo with a
web URL, pressing `o` opens that URL in the operator's default
browser. Non-GitHub / folder / missing mounts emit an "no GitHub
URL for this mount" toast so the hint is discoverable; the sentinel
"+ Add mount" row is a silent no-op.
- `contextual_row_items` now composes an `o open in GitHub` item
onto the existing `d remove · a add` pair when the current row is
a GitHub mount. List-view mounts pane is unchanged — the `o` key
only binds in the editor.
- Tests: `github_mount_row_includes_open_in_github_hint` and
`non_github_mount_row_omits_open_in_github_hint` pin the footer
composition. No unit test for the browser side-effect itself.
* feat(launch): flag git repos in FileBrowser and offer mount-or-dive prompt
Part A — directory listing:
- `annotate_file` (new filter_map body) stats each directory for a
`.git` child and appends U+25A3 (▣) to the display name when present.
Works for plain clones (`.git` is a directory) and submodules (`.git`
is a file containing `gitdir: …`). Single stat per entry — no
recursive walk.
Part B — Enter on a git-repo row:
- New `GitPromptFocus { MountHere, EnterIn, Cancel }` + two fields on
`FileBrowserState` (`pending_git_prompt`, `pending_git_focus`) drive
an in-widget confirm overlay. Enter on a git-repo row opens the
prompt; Tab/←→/h/l cycle focus; Enter commits the focused option;
M/E/C are direct shortcuts; Esc dismisses the prompt without
cancelling the browser.
- MountHere commits the repo path through the same sandbox rules as
`s` (rejects root / `~/.jackin/*`). EnterIn navigates into the repo
via `explorer.set_cwd` (avoids re-posting Enter, which would re-open
the prompt). Cancel just clears state. Non-git folders keep their
usual Enter-navigates-in behavior.
- Overlay renders as a centred 3-button bar inside the explorer area
so the listing stays visible as context. Phosphor palette + focus
styling mirrors `confirm.rs`/`save_discard.rs`; button ring copied
locally rather than cross-importing between widgets.
- Footer legend swaps to `Tab cycle · Enter confirm · Esc cancel`
while the prompt is active.
Tests cover: marker on `.git`-dir and submodule-`.git`-file cases, no
marker on plain folder, Enter opens prompt on repo row, MountHere
commits the path, EnterIn navigates in and clears prompt, Cancel
clears without cwd change, Esc dismisses prompt without cancelling
browser, plain-folder Enter navigates as before, and the `M` shortcut
commits regardless of current focus.
* feat(launch): add mount-destination choice modal widget
Introduces the `mount_dst_choice` widget and wires a new
`Modal::MountDstChoice` variant into the manager's modal enum plus
render dispatcher. No input behaviour changes yet — follow-up commits
swap the Editor and Prelude FileBrowser→TextInput chains to route
through this modal.
The widget is the 3-button focus-ring pattern pioneered by
`save_discard`: default focus on `OK`, Tab/BackTab cycling, and single-
letter shortcuts (`o`/`e`/`c`). Default on `OK` because the common case
is `dst = src`, so an accidental Enter commits that without surprise.
* feat(launch): route editor add-mount through destination choice modal
The FileBrowser→TextInput chain in the Editor's Mounts tab assumed
every operator wanted to edit the destination path. In practice, 95%
of mounts commit with dst = src. Swapping in the new MountDstChoice
modal makes the common path a single Enter press and keeps the old
behaviour one keystroke away via `Edit destination`.
`apply_file_browser_to_editor` now opens MountDstChoice instead of
pushing a provisional mount plus TextInput. The actual push happens
in the MountDstChoice commit handler:
- OK: push MountConfig { src, dst = src, rw }, close modal.
- Edit destination: push the provisional mount (as today) and open
Modal::TextInput{MountDst} pre-filled with src. The existing
TextInputTarget::MountDst handler overwrites the provisional dst.
- Cancel / Esc: close the modal, leave pending.mounts untouched.
Behavioral tests pin all three paths and guarantee no mount is
pushed until the operator commits in the choice modal.
* feat(launch): route create-prelude add-mount through destination choice
Mirrors the editor-side change: the Create wizard's FileBrowser step no
longer assumes the operator wants to edit the destination. Instead, the
prelude now opens MountDstChoice after FileBrowser commits, offering
the fast `OK` path that skips TextInput entirely.
Both paths (OK and TextInputDst commit) share a new helper
`prelude_advance_to_workdir_pick` so the downstream WorkdirPick stage
receives the same staged mount regardless of whether the operator
edited the destination. This keeps the chain FileBrowser → (choice) →
WorkdirPick → TextInput(Name) intact for the `OK` shortcut.
Cancel on MountDstChoice matches today's Esc-during-TextInput
behaviour: close the modal, leave the prelude state alone so the
outer dispatcher treats it as a wizard-cancellation and returns to
the manager list.
* refactor(launch): extract mount-dst-choice dispatch to keep clippy happy
The inline `Modal::MountDstChoice` arm inside `handle_editor_modal`
pushed the function above clippy's 100-line ceiling. Extract the
outcome dispatch into `dispatch_editor_mount_dst_choice` and tidy the
helper's doc comment so `TextInput` doesn't trip the missing-backticks
lint. No behavioural change.
* fix(launch): strip trailing slash in FileBrowser name filter
ratatui-explorer appends `/` to directory names at runtime, so the
filter in `annotate_file` was comparing `"Library"` against `"Library/"`
and silently letting every excluded entry render. Same bug let `..`
through on the sandbox-escape check. Normalize with
`trim_end_matches('/')` before matching, and harden the `s`/Enter
paths + default key dispatch to guard against empty listings (the
fixed filter can now produce an empty `files()` which made
`current()` and nav-key dispatch panic inside ratatui-explorer).
* polish(launch): simplify Destination modal title
Rename the mount-destination TextInput label from
`destination (default: same as host path)` to plain `Destination`.
The parenthetical hint is redundant after batch 12: the TextInput
only opens when the operator explicitly picks "Edit destination"
on the MountDstChoice modal, so they're already in deliberate-edit
mode with the src pre-filled as the default.
Also capitalizes the title to match the other modal block titles
(Confirm, Unsaved changes, Mount destination, Git repo detected,
Workdir pick, Rename workspace, Name this workspace). No other
titles needed changes — the audit was clean.
* feat(launch): bind Left/Right to prev/next tab in Editor
Extend `handle_editor_key` so Right matches Tab (forward cycle) and
Left matches BackTab (reverse cycle). Wrap-around behavior mirrors
the existing Tab contract: General → Mounts → Agents → Secrets →
General, and symmetrically for reverse.
Modal-open precedence is already guarded by the early-return in
`handle_key` — Left/Right continue to feed into modal handlers
(Confirm, SaveDiscard, MountDstChoice) when a modal is active.
* feat(launch): wire list-view `o` to open workspace GitHub mounts
Extend the `o` key beyond the Editor's Mounts tab to the workspace
list view. On a saved workspace row:
0 GitHub mounts → toast "no GitHub URLs for this workspace"
1 GitHub mount → open::that_detached immediately
≥2 GitHub mounts → open a new GithubPicker modal; Enter commits
the highlighted URL to open::that_detached.
Row 0 (Current directory) and the `+ New workspace` sentinel toast
`no workspace selected` for discoverability. The list footer now
surfaces `o open in GitHub` only on rows whose workspace resolves
to ≥1 GitHub-hosted mount.
Adds:
- new `widgets/github_picker.rs` widget (title-styling and tab-list
pattern mirror WorkdirPick so the modal feels native);
- `Modal::GithubPicker { state }` variant, with arms closed in the
render-size switch, `handle_editor_modal` (defensive cancel), and
a new `handle_list_modal` dispatcher;
- `list_modal: Option<Modal<'a>>` slot on ManagerState — list-view
modals weren't previously anchored anywhere; Editor/CreatePrelude
keep their per-stage modal slots unchanged;
- `resolve_github_mounts_for_workspace` helper, shared by the input
handler and the render-side footer-hint guard.
Piggybacks a one-line clippy fix in file_browser.rs
(`iter().any(|x| *x == bare)` → `EXCLUDED.contains(&bare)`) that
surfaced after the trailing-slash filter landed.
* fix(launch): drop cwd suffix from Current directory row label
The list row for the synthetic "Current directory" choice used to read
`Current directory (~/Projects/foo)`. The right-pane details already
show the cwd on the `workdir` line, so the parenthetical suffix is
duplicate visual load. Render just `Current directory`.
Row 0 keeps its WHITE colour so the synthetic choice still visually
separates from the phosphor-green saved workspaces below it.
* fix(launch): align mount-table type column with header
MOUNT_MODE_COL_WIDTH was 2, matching the literal width of rw/ro but
leaving a 2-char gap before the data row's kind column versus the
header's 4-char "mode" label. Header and data rows shared the same
"{mode:<mw} type" format string but MOUNT_MODE_COL_WIDTH no longer
matched the header label length, so `type` and its data (e.g. "folder")
rendered at different offsets.
Pin MOUNT_MODE_COL_WIDTH to 4 so rw/ro pad to the header's "mode"
width. Both the header and data emit the same two-space gutter before
the `type` column, so the kind label lines up with the header offset.
Extend the existing gap-between-mode-and-type test to additionally
assert that `header.find("type") == data.find("folder")` — the
type-column offset must match for a row with a plain folder mount.
* refactor(launch): drop dead OSC 8 hyperlink scaffolding
The `o`-opens-in-system-browser path (via open::that_detached)
supplanted the aspirational OSC 8 hyperlinks-in-terminal route. The
OSC 8 helpers were already #[allow(dead_code)] — remove them:
- `osc8_link()` — wrapped text in OSC 8 ESC sequences;
- `MountKind::labeled_hyperlink()` — built a GitHub-linked label
from a branch/sha + url;
- their associated NOTE blocks on render.rs (mounts subpanel) and
the agents-hyperlink TODO next to render_agents_subpanel.
The `web_url: Option<String>` field stays — the `o` key consumes
it to open the branch URL. Likewise `remote_to_web`,
`parse_remote_origin_url`, and the `GitHost::Github` classification
are all still in the live path.
Clippy baseline (4) unchanged; no tests touched.
* feat(launch): mouse-draggable list/details divider
Add a draggable seam between the workspace list pane and the details
pane in the manager TUI. Click-and-drag on the seam column (within ±1)
resizes the split; the percentage is clamped to [20, 80] so neither
pane can be starved.
Mechanically:
- ManagerState gains `list_split_pct: u16` (default 45) and
`drag_state: Option<DragState>`. `clamp_split` + split-range consts
live alongside. `render_list_body` reads `list_split_pct` instead
of the hard-coded 45/55.
- `src/launch/mod.rs` enables `EnableMouseCapture` after entering the
alternate screen and `DisableMouseCapture` in the terminal guard's
Drop. Side-effect: the terminal's native click-drag text selection
stops working while the TUI is running — hold Shift (Terminal.app,
iTerm2) or Option (iTerm2) to bypass. Documented inline.
- The run-loop now matches on `Event::{Key, Mouse, _}` (was a bare
`if let Event::Key`). Mouse events in the Manager stage dispatch
to a new `manager::input::handle_mouse` with the current terminal
size as a `ratatui::layout::Rect`.
- `handle_mouse` hit-tests the seam, captures a `DragState` anchor
on `Down(Left)`, updates `list_split_pct` on `Drag(Left)`, and
clears the anchor on `Up(Left)`. It also gates on List stage, no
open list-modal, and `term_size.width >= 40`.
Unit tests (8 new, pure state manipulation — no ratatui loop):
- mouse_down_on_seam_starts_drag
- mouse_drag_updates_split_pct
- mouse_drag_clamps_to_min_and_max
- mouse_up_ends_drag
- mouse_down_far_from_seam_does_not_start_drag
- drag_ignored_when_list_modal_open
- drag_ignored_on_non_list_stage
- drag_ignored_when_terminal_too_narrow
Clippy baseline (4) unchanged.
* fix(launch): rename current-dir pane first block to "General"
The synthetic "Current directory" row has a right-pane first block titled
" Current directory ", but the left-list row label already conveys that
context. Rename to " General " to match the saved-workspace details pane
(General / Mounts / Agents) so both panes use the same three sub-panel
titles.
* fix(launch): remove phantom empty row in current-dir Mounts block
`render_current_dir_details_pane` hard-coded the Mounts block at
`Constraint::Length(5)`, which over-allocated by one row for the
single-mount current-directory case and left a visible empty line
inside the block border.
Extract the height formula from `render_details_pane` into a shared
`mount_block_height` helper (2 borders + 1 header + max(1, N) data rows,
clamped to 12) and use it from both pane renderers so the two paths
produce identically-tight Mounts blocks.
Covered by four regression tests pinning the formula for the empty,
single, multi, and many-mount cases.
* fix(launch): align General sub-panel content with Mounts/Agents
The General sub-panel (on both the saved-workspace pane and the
current-directory pane) rendered its `workdir`/`last` rows flush against
the block's left border, while the Mounts and Agents sub-panels already
used a two-space indent. The mismatch gave the right pane a jagged left
edge across the three stacked blocks.
Add the same two-space prefix to the General rows on both panes. The
convention is pinned by a new `SUBPANEL_CONTENT_INDENT` constant and
two visual regression tests that render each sub-panel to a
`TestBackend` buffer and assert the first visible character of row 0
sits at that indent relative to the block's left border. Covers the
"any agent" fallback and the starred-default-agent row explicitly.
* fix(launch): follow worktree commondir for GitHost detection
Git worktrees are checkouts whose `.git` is a file pointing at
`<main>/.git/worktrees/<name>`. That per-worktree gitdir owns HEAD but
has no `config` of its own — the shared config (including the remote
URL) lives at the target of a `commondir` pointer. The previous
`resolve_git_dir` stopped at the worktree-specific gitdir, so
`resolve_host_and_url` read nothing, `GitHost` defaulted to `Other`,
and the label rendered as `git · branch` instead of `github · branch`
for every worktree of a GitHub-hosted repo.
Split resolution into `resolve_gitdirs`, which returns a pair:
- `work_dir` — owns HEAD (worktree-specific for worktrees, identical
to `config_dir` for plain clones and submodules).
- `config_dir` — owns the remote URL (follows `commondir` when present,
handling both relative and absolute pointer forms).
`inspect` now parses HEAD from `work_dir` and the remote URL from
`config_dir`, so the host is re-classified correctly for worktrees
without perturbing the submodule path.
Covered by three new tests:
- `worktree_gitfile_resolves_to_commondir` (relative commondir)
- `worktree_commondir_with_absolute_path`
- `submodule_gitfile_still_resolves_host_end_to_end` (regression guard
for submodules — HEAD + config co-located, no commondir)
* refactor(launch): drop FileBrowser affordance banner
The "press [S] to use this folder" banner above the explorer is redundant
with the `S select` footer hint below it, and inconsistent with other
modal styling (no other modal has a top banner). Drop it and its layout
constraint so the explorer shifts up by one row.
The `rejected_reason` banner (shown when the operator picks $HOME itself
or a `.jackin/` path) stays — it is functional error feedback, not an
affordance hint.
* refactor(launch): prefix git marker, "repo" -> "repository" in UI
Three small FileBrowser/git-prompt polish items:
- Prepend the git-repo marker instead of appending. Changed the glyph
from U+25A3 (white square with black small square) to U+2387
(alternative key symbol — reads as a branch) and moved it to the head
of the directory name so the eye lands on it first. The trailing
marker was easy to miss; a file listing now shows
"⎇ scentbird-root/" rather than "scentbird-root/ ▣".
Noted as a one-liner in the code: per-entry colouring would need
dropping ratatui-explorer — out of scope here.
- Rename user-facing "repo" to "repository". The button label becomes
"Mount this repository" and the prompt title becomes
"Git repository detected". Identifiers (repo_dir, GIT_REPO_MARKER,
test fixture names) are left alone — this is a UI-string change only.
- Rename the middle git-prompt button from "Enter to pick subdirectory"
to "Pick a subdirectory" — imperative voice parallel to the first
button, no more "key + verb" mix. Footer hints under the prompt
still read "E enter" for the shortcut, which remains correct.
* refactor(launch): uppercase single-letter hotkeys in footer hints
Operator prefers the `M mount · E enter · C/Esc cancel` style
consistently across the TUI. Previously the list-view footer was
lowercase (`e edit · n new · d delete · o open in GitHub · q quit`)
while the git-prompt hint was uppercase. Normalise every footer site
on uppercase single-letter keys; multi-character glyphs (Enter, Tab,
Esc, ↑↓, etc.) and non-alpha keys (`*`) pass through unchanged.
Updates:
- `src/launch/manager/render.rs` list footer: E/N/D/O/Q
- `src/launch/manager/render.rs` editor save footer: S
- `src/launch/manager/render.rs` contextual_row_items: D/A/O on
Mounts rows, A on the "+ Add mount" sentinel
- `src/launch/widgets/file_browser.rs` nav hint: S select,
H/← up
- Existing footer test assertions updated to match new casing
- New `footer_hotkeys_are_uppercase` test scans contextual hints
(Mounts row + sentinel, Agents) and verifies every single-char
alphabetic `Key` item is uppercase
Key handlers extended to accept both cases where a footer now shows
uppercase. Most handlers already matched `'e' | 'E'` from batch 11;
the remaining lowercase-only sites (list Q/E/N/D/O, list K/J nav,
editor S/K/J, editor-Mounts A/D/O, file_browser S/H/L nav, picker
J/K) now take `'x' | 'X'`. Behavioural change is nil — Caps Lock
and Shift-held hotkeys now work where they already did at the
footer-advertised case.
* feat(launch): click-to-select in workspace list
Extend `handle_mouse` with row-click selection on the workspace-list
pane. Left-button Down inside the list content area maps to the row
index and updates `ms.selected`; clicks on the seam column still
start a drag (regression guard for batch 14).
Hit-test rules:
- Seam always wins — a click within ±1 column of the current seam
starts a drag regardless of y. This keeps the resize affordance
unambiguous even when the seam overlaps a valid row position.
- Otherwise, clicks inside `[1, seam - 1]` × `[header + 1, body_end - 1]`
(left-pane interior minus borders) convert to a row index via
`mouse.row - (header_height + 1)`.
- The index must be in `[0, sentinel_idx]`; beyond that we silently
drop the click. Row 0 = "Current directory", 1..=N = saved, N+1 =
"+ New workspace" sentinel.
- Clicks outside those ranges (header, footer, borders, right pane)
are ignored.
Layout heights are pulled from two new private consts mirroring
`render::render`'s `Constraint::Length(3)`/`Length(2)`. If the
chrome ever changes shape, both the render and hit-test paths need
updating together.
Double-click = launch is intentionally skipped: crossterm doesn't
emit native double-click events, so implementing it would need
tracking `(last_row, last_instant)` on `ManagerState` with a debounce
window. That's more state-machine than one item in this batch
warrants — left as a follow-up. Single-click-to-select is the
must-have and is fully wired.
Five new tests cover the happy path (row 0, mid-list row, sentinel
row), the negative path (header / borders / right pane / below
sentinel / footer), and the seam precedence regression guard.
* fix(launch): FileBrowser Esc steps back one level when not at root
Previously Esc always cancelled the modal. If the operator drilled into
a subfolder (via Enter on a plain dir or via the git-prompt
"Pick a subdirectory" path), a stray Esc collapsed the whole picker and
returned to the workspace list. Operators expect Esc to back out one
level — mirroring the behavior of `h` / `←` — and only cancel when
already at root.
Esc now:
- Clears any stale rejected_reason.
- Navigates one level up when cwd != root (sandbox-guarded, same as the
existing root-clamp).
- Cancels the modal only when cwd == root.
Git-prompt Esc is unchanged: it still dismisses only the prompt and
leaves the explorer open at the current cwd.
Footer hint updated from "Esc cancel" to "Esc up/cancel" (matching the
batch 16 uppercase-hotkey convention) — accurate for both drilled-in
and root cases.
Five new tests cover: esc-at-root cancels, esc-in-subfolder navigates
up, esc-three-levels-deep goes up exactly one, esc clears
rejected_reason, and the git-prompt Esc regression guard.
* refactor(launch): default workspace list/details split 45 -> 30
Workspace names like `chainargos-blockchain-nodes` fit comfortably at
30%; the details pane gets the breathing room for git branches and
full paths. MIN/MAX bounds (20/80) stay unchanged.
Also refactor the seam drag-and-select tests in `input.rs` to refer to
`DEFAULT_SPLIT_PCT` instead of the literal `45`, so future changes to
the default don't silently break the test assumptions about the seam
column.
* refactor(launch): agents subpanel moves default-marker star to trailing position
The read-only Agents sub-panel in the list/details pane previously placed
a leading star prefix on the default-agent row — "★ alpha" — so default
rows rendered at col 4 while non-default rows rendered at col 2. Move the
star to a trailing position after the name, separated by a space, so
every agent name starts at `SUBPANEL_CONTENT_INDENT` (col 2) matching
the General and Mounts sub-panels' leading-indent convention.
The star renders as its own `Span` styled with `PHOSPHOR_DIM` — keeps
the agent-name base color (`PHOSPHOR_GREEN` when registered globally,
`PHOSPHOR_DIM` when not) untouched and keeps the marker low-chrome.
The Editor tab's Agents view (`render_agents_tab`) is intentionally
untouched — that layout carries `[x]`/`[ ]` toggles with a different
selection affordance and is a separate concern.
Replaces the legacy `agent_star_row_aligns_with_content_column` test
with three focused tests covering name alignment on non-default rows,
trailing-star position on default rows, and name alignment on default
rows (independent of the trailing star).
* feat(launch): show origin URL in FileBrowser git-repo prompt
Enter on a git-repo folder now opens the "Git repository detected"
overlay with the origin web URL displayed between the title and the
question. The URL is resolved via `mount_info::inspect` when the
prompt opens and cleared when the prompt is dismissed, so stale URLs
don't leak between repos.
Non-GitHub remotes (and repos with no origin) resolve to `None` and
the URL row is elided entirely — no "(no URL)" noise. The overlay
height grows by one row only when a URL is present.
* feat(launch): Esc rewinds the create-workspace wizard one step
Esc at any step of the create-workspace prelude now steps back to
the previous step instead of abandoning the whole wizard:
- FileBrowserSrc (step 1) → workspace list (no prior state)
- MountDstChoice → reopen FileBrowserSrc at the last cwd
- TextInputDst → reopen MountDstChoice
- WorkdirPick → reopen MountDstChoice or TextInputDst
depending on which dst-path was taken
- TextInputName → reopen WorkdirPick
`CreatePreludeState` grows two breadcrumb fields (`last_browser_cwd`,
`used_edit_dst`). `FileBrowserState` exposes `cwd()` / `set_cwd()`
so the wizard can restore the exact directory the operator was
browsing when src was committed — no more starting back at $HOME.
* refactor(launch): replace ratatui-explorer with a custom file browser
ratatui-explorer 0.3's Theme exposes only a single `dir_style` shared
by every directory row — meaning the operator affordance "highlight
git repos differently" was impossible without a rewrite. The custom
browser renders each row directly via tui-widget-list + Paragraph, so
git rows can carry a distinct trailing ` (git)` suffix in phosphor-dim
bold while plain folders stay plain phosphor green.
Other gains vs the ratatui-explorer wrapper:
- `h/l`, arrows, `s`, `Esc`, and Enter are handled directly — no more
round-tripping through `FileExplorer::handle` with div-by-zero guards
on empty listings.
- `cwd()` / `set_cwd()` are first-class methods, used by the create-
workspace wizard's step-back navigation.
- `FolderEntry { is_git }` replaces the fragile U+2387 prefix hack.
- The EXCLUDED-at-root / sandbox / $HOME-clamp / `~/.jackin` rejection
behaviour is preserved exactly.
The ratatui-explorer dependency is dropped. Every existing FileBrowser
test was ported; three new tests cover per-entry is_git tagging and
the rendered ` (git)` suffix.
* style(launch): space-pad modal block titles
TextInput and WorkdirPick titles previously rendered flush to the
top-left border (`┌Name this workspace...`). Wrap them in leading
and trailing spaces so every modal renders with the canonical
`┌ Title ─...` padding, matching SaveDiscardCancel / Confirm /
MountDstChoice / GithubPicker / FileBrowser.
* refactor(launch): rename workdir to working dir in UI strings
User-facing occurrences of "workdir" now read as "working dir" (in
narrow label columns where column alignment matters) or "Working
directory" (in modal titles and contextual footer hints). Struct
field names, method names, variable names, test identifiers, and
other non-UI uses keep the shorter "workdir" spelling.
Touched:
- WorkdirPick modal title becomes "Working directory — pick from mounts".
- General sub-panel label becomes "working dir " (col-padded to 12 to
match a widened "last " column).
- General-tab editor rows use "working dir" (fits existing 15-wide
label padding).
- Footer hints become "Enter pick working directory".
- Launch preview sidebar label becomes "work dir " (matches the 9-wide
"agent " column in the same pane).
* style(launch): align every popup to the canonical modal template
Operator feedback: popups looked inconsistent and that made the whole
UI feel messy. Pull every modal onto one shared visual contract so a
future widget has an obvious shape to follow.
Canonical template (modelled on SaveDiscardCancel):
- Border: PHOSPHOR_DARK (0,80,18) — dim enough that the title + focus
highlight pop.
- Title: Title Case, wrapped in leading + trailing spaces, WHITE+BOLD.
- Hint row (PHOSPHOR_DIM separators between groups): WHITE+BOLD keys
and PHOSPHOR_GREEN labels, canonical verbs per modal kind.
- Choice buttons: focused = WHITE bg / Black fg / BOLD; unfocused =
PHOSPHOR_GREEN fg / BOLD (no bg) so only the focused choice pops.
4-space gap between buttons, 2-space padding inside each label.
Per-widget changes:
- save_discard: border green -> dark. Unfocused buttons drop the
phosphor bg. Sep variable uses phosphor_dark locally.
- confirm: border green -> dark. Unfocused Yes/No buttons drop the
phosphor bg. Footer styles re-use the local constants instead of
inlined RGB literals.
- mount_dst_choice: unfocused buttons drop the phosphor bg. Border
was already dark.
- text_input: border green -> dark. Hint moves inside the bordered
block so the bottom border is unbroken. Inner layout is top pad /
input / spacer / hint (canonical). Render-table bumps TextInput
height from 5 to 6.
- workdir_pick: border green -> dark. Adds blank top padding, blank
spacer, and a canonical navigate/confirm/cancel hint row (was
missing entirely).
- github_picker: border green -> dark. Same inner layout as
workdir_pick (blank / list / blank / hint). Height table adjusts
the `+4` chrome allowance to `+5` for the hint row.
- file_browser git-repo overlay: border green -> dark. Buttons drop
the phosphor bg. Key binding for "Pick a subdirectory" renames
from `e/E` to `p/P` so the hint (`M mount · P pick · C/Esc cancel`)
matches the button label. Hint gets the canonical `Tab cycle ·
Enter confirm` prefix. Overlay widens from 60 to 80 so the three
buttons + canonical hint line both fit on one line without wrapping.
New consistency tests in widgets/mod.rs pin the contract:
- all_modal_block_titles_have_padding
- all_modal_borders_are_phosphor_dark
- all_modal_hint_rows_use_canonical_styles
Each iterates every modal render and asserts the shared invariants.
* style(launch): strip 'Tab cycle' …
donbeave
added a commit
that referenced
this pull request
May 7, 2026
* docs(specs): workspace manager TUI (PR 2 of 3) Design spec for PR 2 of the launcher-workspace-manager series. Adds an interactive workspace manager screen to the jackin launcher — list, create, edit, and delete workspaces without dropping to CLI. Reached via `m` from the existing Workspace picker; Esc returns to the launcher. Launch path stays keystroke-identical. Key design decisions from brainstorming (all settled, no open questions): - Entry model: separate Manager screen on `m` keypress; launch path unchanged. - Editor tab set: General · Mounts · Agents · Secrets-stub. Secrets placeholder locks in the final tab strip so PR 3 fills in the panel without a visual reshuffle. - Text-edit UX: modal push — centered overlay, one reusable TextInput widget. - Staging: explicit save via `s`. Pending changes drive dirty markers; Esc with pending opens Discard/Save/Cancel. - Create flow: mounts-first wizard — file browser for host source, dst auto-defaulted to the same absolute path as src (host-path mirror), workdir picked from mount dsts + ancestors (never free-text), name last with live uniqueness check. - Delete UX: single-line Y/N confirm modal. - Style: reuses jackin's existing digital_rain (src/tui/animation.rs), step_shimmer, spin_wait, and landing-page color tokens from docs/src/components/landing/styles.css. One new area-bounded rain widget extracted from animation.rs. Three new reusable widgets emerge (TextInput, FileBrowser, Confirm) that PR 3's Secrets tab will consume unchanged. All persisted writes flow through ConfigEditor (established in PR 1, merged in #162). Non-goals: per-(workspace × agent) env overrides (PR 3), global mount management (CLI only), agent lifecycle from manager (CLI only), CLI surface changes, CHANGELOG. * docs(specs): lock third-party widget choices for PR 2 Amends the workspace manager TUI spec with a Third-party dependencies subsection that names the three ratatui ecosystem crates we'll adopt: - ratatui-textarea (v0.9.x) — single-line TextInput (ratatui-org owned) - ratatui-explorer (v0.3.x) — FileBrowser with folders-only wrapper - tui-widget-list (v0.15.x) — WorkdirPick list mechanics All three require the ratatui unstable-widget-ref feature flag. Rejected with rationale so reviewers don't re-litigate: tui-input (superseded by ratatui-textarea), tui-confirm-dialog / tui-overlay (Confirm modal is cheaper hand-rolled), rat-widget (too opinionated), throbber-widgets-tui / ratatui-cheese (we have spin_wait already), ratatui-toaster (banner is ~30 LOC with step_shimmer), tui-logger (jackin has no log or tracing framework today). Also updates Rollout section — "no new dependencies" was no longer accurate. --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com>
donbeave
added a commit
that referenced
this pull request
May 7, 2026
* docs(plans): workspace manager TUI implementation plan
Twenty-two-task TDD-shaped implementation plan for the workspace
manager TUI specced in #164. Ordered in seven phases:
1. Foundation — deps + module scaffolds + animation.rs refactor
2. Widgets — Confirm, TextInput, FileBrowser, WorkdirPick, PanelRain
3. State machine — ManagerState, EditorState, CreatePreludeState
4. Render — list view, editor (4 tabs), modal dispatcher
5. Input — modal-first key dispatch with per-stage routing
6. Integration — LaunchStage::Manager wire-in, m keybinding, full
editor + create key handling, ConfigEditor save/create/delete paths
7. Polish — style effects (boot reveal, save shimmer, toast expire),
integration test, final verification + PR
Each task is TDD-shaped: write failing test → run fails → implement →
run passes → commit. Complete code in every implementation step. No
placeholders.
Scope cut documented in self-review: tab-slider and panel-focus-glow
animations from the spec's Style section are omitted; they're cosmetic
and can land in a follow-up PR without rework.
* feat(launch): scaffold workspace manager modules + widget deps
Adds three ratatui ecosystem crates (ratatui-textarea 0.9,
ratatui-explorer 0.3, tui-widget-list 0.15) and enables ratatui's
unstable-widget-ref feature. Creates empty module structures at
src/launch/widgets/ and src/launch/manager/ to land typed setters,
widgets, and state transitions in subsequent commits.
No behavior change.
* refactor(tui): extract render_rain_frame for reuse
Separates the per-frame rain rendering from digital_rain's event loop
so the upcoming PanelRain widget can render bounded-area rain without
duplicating the renderer. tick_rain and RainState become pub(crate)
for the same reason. Fullscreen digital_rain is rewritten to delegate
to render_rain_frame. No visible change.
* feat(launch): Confirm widget — Y/N modal
Hand-rolled Y/N confirmation dialog. Case-insensitive, Esc cancels.
~60 LOC + 5 tests. Used by delete-workspace and discard-changes flows.
* feat(launch): TextInput widget — single-line via ratatui-textarea
Wraps TextArea in single-line mode (intercepts Enter and Ctrl+M so
newlines are never inserted). Exposes a ModalOutcome<String> contract:
Enter commits, Esc cancels, everything else passes through to the
textarea for cursor / insert / backspace handling.
Cursor is placed at end of initial text on construction so editing
feels natural (backspace works immediately on prefilled values).
* feat(launch): FileBrowser widget — wraps ratatui-explorer
Folders-only filter, seeded from $HOME by default, adds 's' as
select-current-folder. Delegates all navigation (h/l/j/k/Enter/
Backspace/Home/End/PgUp/PgDn/Ctrl+h) to ratatui-explorer defaults.
* feat(launch): WorkdirPick widget — choice list via tui-widget-list
Derives the pick list from mount dsts + each ancestor up to /, with
labels (mount dst / parent / root). Deduplicates when multiple mounts
share ancestors. Enter commits selected path, Esc cancels.
* feat(launch): PanelRain widget — area-bounded phosphor rain
Wraps tui::animation's RainState engine for rendering into a bounded
Rect. Tick + render are separate so callers control frame rate.
Resizes state when the rect changes shape.
Adds RainState::new(cols, rows) constructor to animation.rs so the
widget can initialize state without duplicating the column/grid setup
that was previously inlined in digital_rain().
* feat(launch): ManagerState + EditorState + CreatePreludeState types
Defines the top-level state machine per spec § 3: ManagerStage enum
(List / Editor / CreatePrelude / ConfirmDelete), EditorState with
dirty detection and change_count, CreatePreludeState with the
mounts-first wizard step enum, Modal enum with target enums, Toast
type, and constructors. Tests cover WorkspaceSummary derivation,
ManagerState::from_config, EditorState dirty detection, and
CreatePreludeState initial step.
ManagerStage and ManagerState carry a lifetime parameter propagated
from TextInputState<'a> (ratatui-textarea borrow). MountConfig lacks
Ord/Hash so change_count uses linear containment checks rather than
BTreeSet symmetric_difference.
Transitions and key handling are filled in by subsequent tasks.
* feat(launch): create-workspace wizard state transitions
Mounts-first flow: PickFirstMountSrc → PickFirstMountDst → PickWorkdir →
NameWorkspace. Each accept_* method advances the step. default_mount_dst
mirrors the host src path. default_name derives from the dst basename.
build_workspace assembles the final WorkspaceConfig.
* feat(launch): manager list view render
Renders ManagerStage::List: header banner, horizontal-split body
(workspace list + details pane), footer hint. Other stages rendered
by subsequent tasks (12: editor, 13: modal dispatcher).
* feat(launch): editor view render (all four tabs)
Renders Editor stage with General / Mounts / Agents / Secrets-stub
tabs, dirty markers on changed fields, save-count footer. Error
banner overlays the top of the tab body using --landing-danger
(#ff5e7a) for real errors.
Refactors top-level render to let stages declare whether they use
shared chrome (List, future ConfirmDelete) or their own full-screen
layout (Editor, future CreatePrelude).
* feat(launch): modal render dispatcher
Centers a modal Rect at 60x30 percent of the frame and dispatches to
the appropriate widget's render function based on the Modal variant.
* feat(launch): key dispatcher with modal precedence
Scaffolds handle_key with modal-first precedence: if a modal is open
anywhere in the state machine, events route to the modal handler
before per-stage handlers. Full editor + prelude wiring lands in
Tasks 16 / 17; this commit has stubs for those to keep the compiler
happy. List and ConfirmDelete stages are fully wired (navigation,
delete flow via ConfigEditor).
* feat(launch): LaunchStage::Manager + m keybinding
Adds a third launch stage and wires an m keypress from the Workspace
picker to transition into it. run_launch now takes AppConfig by value
+ &JackinPaths so the manager can open ConfigEditor. Footer hint in
the Workspace stage gains 'm manage'.
* feat(launch): editor key handling — tabs, save, discard, field edits
Implements Tab/Shift-Tab navigation between tabs, ↑↓ row selection,
Enter-to-edit (opens modal per field type), Space/* on Agents tab,
a/d on Mounts tab. s triggers save via ConfigEditor::edit_workspace
or create_workspace, with error banner on failure. Esc with pending
changes opens the Discard confirm modal; Esc with clean state returns
to the manager list.
* feat(launch): full create-workspace prelude flow
Chains the four modals (file browser → dst TextInput → workdir pick →
name TextInput) through CreatePreludeState. On completion, transitions
to Editor(mode=Create) with everything pre-populated. s in the editor
creates via ConfigEditor::create_workspace.
* feat(launch): manager style effects
Boot reveal on manager entry via tui::animation::digital_rain(400, None).
Save toast auto-expires after 3s. Shimmer: toast text flashes white during
the first 400ms post-show. JACKIN_NO_ANIMATIONS=1 disables the rain
transition.
* test(launch): end-to-end manager delete-workspace flow
Drives manager::handle_key with scripted key events (d, y). Asserts
the workspace is removed from on-disk config, the manager transitions
back to List, and the in-memory workspace list refreshes. Regression
guard against state-machine drift.
* style(launch): quiet clippy / fmt for workspace manager
Fix all clippy warnings introduced by the workspace-manager TUI (~1500
lines of new code): unnested or-patterns, collapsible-if, elidable
lifetimes, default-trait-access, items-after-statements, match-for-
equality, match-for-single-pattern, needless-pass-by-ref-mut,
doc-markdown (missing backticks), missing-const-for-fn, uninlined-
format-args, manual-Debug-non-exhaustive, large-enum-variant (allow),
too-many-lines (allow), unnecessary-trailing-comma, and the associated
fmt diff (11 files reformatted).
No logic changes; all tests pass (workspace_config_crud requires
--test-threads=1 due to pre-existing set_current_dir race).
* feat(launch): replace Workspace picker with manager as initial stage
The old Workspace picker stage is removed. LaunchStage::Manager becomes
the initial stage — jackin opens directly to the manager. Enter on a
workspace launches it (via the existing Agent picker); e opens the
editor; n creates; d deletes; q/Esc exits jackin. The m keybind is gone
— nothing to enter since we are already in the manager.
Esc from the Agent picker returns to the manager list (was: Workspace
picker, which no longer exists).
Also removes the mid-loop digital_rain(400, None) boot reveal that was
fighting with skippable_sleep's raw-mode toggling, which caused arrow
keys to print as raw escape sequences in the manager instead of being
captured by crossterm events.
* fix(launch): render active modals in manager
render_modal was defined but never called. Modals in Editor and
CreatePrelude stages transitioned correctly in state but had no
visible effect on screen — pressing n would silently put the user in
the create wizard with an invisible FileBrowser, making the create
and edit-field flows appear broken.
Also renders the ConfirmDelete variant's confirm modal directly
(ConfirmState on ConfirmDelete is a top-level field, not wrapped in
Modal::Confirm).
* fix(launch): modal footer hints + stage-aware manager footer
File browser and text input modals were rendering without any hint
about the keys to commit/cancel. Users opening the create workspace
flow saw the folder picker but couldn't progress — pressing Enter
only descended into folders (s is the select key for ratatui-explorer),
and there was no visual cue about the right key.
Adds a one-line phosphor-dim italic footer inside each modal:
- FileBrowser: ↑↓ navigate · Enter open · h/← up · s select · Esc cancel
- TextInput: Enter confirm · Esc cancel
Also makes the top-level manager footer hint stage-aware:
- List stage: existing navigation hint (unchanged)
- CreatePrelude: Create workspace · follow the prompts · Esc cancel
- ConfirmDelete: Y yes · N no · Esc cancel
- Editor: still delegates to render_editor's own footer
* fix(launch): Esc in create wizard returns to list immediately
Previously pressing Esc inside the first create-wizard modal cleared
the modal but left the state machine stuck in ManagerStage::CreatePrelude
with no modal active — render drew a blank body, requiring a second Esc
to reach the non-modal prelude handler that transitions back to List.
Now the post-modal check distinguishes three outcomes: in-progress
(modal still open), complete (wizard finished with name), and
cancelled (modal cleared without a name). Cancelled transitions to
List in the same input pass.
* feat(launch): wire rename + render all agents on Agents tab
Fixes two spec gaps:
1. Agents tab rendered only currently-allowed agents, so the user
couldn't add agents via Space toggle — non-allowed agents were
invisible. Now iterates config.agents (the full set) and shows
[x] or [ ] per agent based on pending.allowed_agents membership.
Threads &AppConfig through manager::render → render_editor →
render_agents_tab. Also fixes set_default_agent_at_cursor to use
config.agents for cursor-to-agent resolution instead of the
allowed-only list.
2. Workspace rename was a TODO. Now:
- ConfigEditor gains rename_workspace(old, new) using toml_edit's
key-rename (preserves nested tables + array-of-tables). Rejects
empty new name, collision, and missing old name.
- General tab's name row is editable on Enter (in Edit mode) via
TextInput modal.
- apply_text_input_to_pending stashes the name on
EditorState::pending_name.
- save_editor calls rename_workspace before edit_workspace when
pending_name differs, then updates editor.mode so subsequent saves
target the new name.
- change_count + is_dirty + render dirty marker all track the rename.
Tests: three new unit tests on ConfigEditor::rename_workspace covering
happy path (nested tables preserved), collision rejection, and empty-
name rejection.
* fix(launch): FileBrowser s commits cwd, not highlighted entry
Pressing s in an empty or file-only folder previously committed the
highlighted entry, which in such a folder is '../' — so the user got
the parent directory, not the folder they were viewing. This was
especially bad for newly-created empty workspace source folders.
Now s commits the explorer's current working directory via
FileExplorer::cwd() (ratatui-explorer 0.3.x). User intent is preserved:
'I've navigated to this folder — select it.' Footer hint updated from
's select' to 's use this folder' to reflect the semantics.
* fix(launch): render active-row cursor in editor tabs
EditorState::active_field tracked the cursor but render functions
didn't display it — users couldn't tell which row Enter / Space / * /
a / d would target. Add a ▸ prefix and phosphor-green bold to the
selected row across all three tabs (General, Mounts, Agents).
Also clamp Down-arrow to the last valid row so the cursor can't run
off the end of the visible content, and thread &AppConfig through to
handle_editor_key's Down handler so it can size the Agents tab's
row count correctly.
* fix(launch): UX polish pass on manager create/edit flows
Nine polish fixes reported after live walkthrough:
1. TextInput/Confirm modals were 30%-of-screen tall, rendered as big
empty boxes around a single line. Now variant-aware: inputs/confirms
are 5-6 rows fixed; file browser and workdir pick stay taller for
their scrolling lists.
2. 'last used' row hidden in Create mode (no history exists).
3. 'default agent' row hidden in Create mode (no agents picked yet).
4. Footer hint is now row-contextual: 'Enter rename' on name row,
'Enter pick workdir' on workdir row, 'a add / d remove' on mounts,
'Space toggle / * set default' on agents, nothing on read-only
rows. Base hint says 's save workspace' (was 's save') for clarity.
5. File browser gets a prominent outer block titled '<cwd> · press
[S] to use this folder' — the select affordance was previously
buried in a dim footer line.
6. Mount rows collapse 'src → dst' to just 'path' when src == dst
(host-path-mirror default — redundant arrow gone).
7. Mounts tab '+ Add / − Remove selected' footer uses white-bold for
the action words to distinguish from the mount list.
8. Agents tab gets a top banner clarifying empty = 'all allowed'
semantics vs non-empty = custom allow-list.
9. Read-only rows (last used) no longer advertise Enter in the footer.
Also fix max_row_for_tab in input.rs: Create mode General tab only
has 2 rows (name read-only + workdir), not 4.
* fix(launch): Confirm modal shows Y/N as styled buttons
Previously the modal rendered '[Y]es · [N]o (default) · Esc cancel' as
inline text inside a tall 30%-of-screen box, which looked like a
multi-line textarea. Now:
- Modal is compact (6 rows)
- Yes/No render as inverted-video buttons, centered
- No (default) uses white-on-black to distinguish as default action
- Esc cancel moves to a dim italic footer hint at the bottom
- Prompt text stays bold-white at the top
Enter intentionally unbound — destructive confirms should not commit
on accidental Enter presses.
* fix(launch): remove nested borders in FileBrowser modal
ratatui-explorer's widget renders its own bordered block with the CWD
as title. The prior polish pass added an outer block with 'press [S]
to use this folder' — the result was double borders, ugly and
confusing.
Drop the outer block. Show the 'press [S]' affordance as a bold-white
centered line ABOVE the explorer (no border), and keep the dim
navigation hint as a line BELOW. The explorer's own cwd-titled block
stays — no nesting.
* feat(launch): Confirm modal gains Tab focus + Enter commits focused
Adds standard confirmation-dialog UX: Tab / Shift+Tab / ←→ / h/l cycle
focus between Yes and No; Enter commits the focused button. Default
focus is No (destructive action protection — accidental Enter won't
commit Yes). Y/N direct shortcuts still work regardless of focus.
Visual: focused button gets white bg + black text + bold; unfocused
gets phosphor-green bg. Footer hint updated to mention Tab + Enter.
Modal grows from 6 to 7 rows for a second spacer between buttons
and hint (was visually cramped).
* fix(launch): shrink FileBrowser + WorkdirPick modal heights
Prior sizing was 60 rows for FileBrowser and 40 rows for WorkdirPick —
effectively fullscreen on a typical 40-50 row terminal. Tight 20 and
12 rows fit comfortably and still show enough entries without the
modal swallowing the whole screen.
* feat(launch): + Add mount is a selectable sentinel row
Mirrors the + New workspace sentinel in the manager list. The Mounts
tab now renders + Add mount as a real selectable row at the end of
the list, selected via ↑↓, activated via Enter. Visual treatment is
white bold (distinguishing it from the green mount rows).
- max_row_for_tab reports len() (mount count + sentinel index) for
Mounts so ↓ can reach the sentinel.
- remove_mount_at_cursor is a no-op on the sentinel (guard already existed).
- a (anywhere on the tab) still works as a quick-add shortcut.
- Contextual footer hint differentiates between 'on a mount row'
(d remove · a add) and 'on the sentinel' (Enter add · a add).
* feat(launch): Agents tab shows [all] / [custom] status badge
Replaces the implicit 'empty list = all allowed' with an explicit
status line at the top of the Agents tab:
Allowed agents: [ all ] (when allowed_agents is empty)
Allowed agents: [ custom ] (3 of 5 allowed) (when non-empty)
The badge is an inverted-video token (phosphor-green bg for 'all',
white bg for 'custom') making the current mode immediately visible.
The agent list below stays as a checklist — toggling updates the
status badge live.
Cursor semantics also shift: cursor is now 0-based into config.agents
(no more header-offset-by-one). toggle_agent_allowed_at_cursor and
set_default_agent_at_cursor are updated accordingly. max_row_for_tab's
Agents arm drops to len()-1.
set_default_agent_at_cursor now also auto-allows the agent being set
as default (was previously a no-op if the agent wasn't already in
allowed_agents).
* feat(launch): Mounts tab shows folder / git · <branch>
Adds a mount_info helper that inspects the host-side src path on
render: checks for .git as dir or submodule-gitfile, reads HEAD, and
reports the current branch (or detached short-sha). Renders next to
each mount row as dim italic metadata:
/Users/…/repo (rw) · git · main
/Users/…/scratch (rw) · folder
/Users/…/gone (rw) · missing
Six unit tests cover: missing path, plain folder, normal repo with
branch, detached HEAD, submodule .git file, label formatting.
* feat(launch): Save/Discard/Cancel modal + richer details pane
Two UX upgrades:
1. Exit-with-changes now offers three explicit choices instead of
binary 'Discard Y/N'. New SaveDiscardCancel modal with three
buttons (Save / Discard / Cancel), Tab cycles focus, Enter commits
the focused option. S/D/C/Esc shortcuts work regardless of focus.
Default focus is Cancel (safest). Save intent triggers ConfigEditor
save → list; Discard just drops pending; Cancel keeps the editor.
2. Manager list's details pane now shows the full mount list (with
folder / git · <branch> labels, same as the Mounts tab) and the
allowed-agents list (or 'any agent' when unrestricted). Title drops
the duplicate workspace name since the list selection already shows
it.
5 new unit tests on SaveDiscardState covering focus cycling and key
shortcuts.
* fix(launch): restrict FileBrowser to \$HOME, rename main title
Four UX fixes:
1. Main manager screen title 'manage workspaces' → 'workspaces'
(the screen does more than manage — launch, create, edit, delete).
2. FileBrowser modal goes fullscreen (100% x 100%) so the main chrome
doesn't peek through and confuse the visual.
3. FileBrowser now:
- Starts at \$HOME (already did)
- Excludes Library, Applications, Movies, Music, OrbStack, Pictures
from the listing via filter_map
- Clamps cwd back to \$HOME if the user escapes above it via
set_cwd() (ratatui-explorer 0.3.x has this method)
- Rejects \$HOME itself as a workspace source
- Rejects ~/.jackin/* (jackin's reserved data area)
4. Rejected selections show an inline red error banner
(#ff5e7a) above the explorer. Cleared on next keypress.
* fix(launch): display paths as ~/… via shorten_home
Paths starting with $HOME now render as '~/...' in the TUI:
General tab workdir, Mounts tab rows, details pane mounts/workdir,
WorkdirPick choices. Consistent with jackin's existing shorten_home
helper (already used elsewhere in the launcher).
Paths stored on disk are unchanged — this is display-only.
* fix(launch): mount table formatting + FileBrowser resize/colors
Two fixes:
1. Mount lists in the details pane and Mounts tab render as an
aligned 3-column table (path, mode, type) instead of a free-form
line where the '(rw)' tag and type metadata floated at variable
positions. shorten_home applied to paths consistently via the
shared format_mount_rows helper, which is called from both
render_details_pane and render_mounts_tab.
2. FileBrowser modal goes from fullscreen (100%) to 70%x70%, letting
the surrounding chrome show again so the dialog reads like a
dialog, not a whole screen. Theme configured to use jackin's
phosphor palette (green text, bright-phosphor highlight, shortened
CWD title via shorten_home in a dynamic with_title_top closure).
* fix(launch): drop Agents 'default' column + cap workspace list height
1. Agents tab header 'allowed? · default · agent' → 'allowed? · agent'.
The star marker next to the agent name already indicates default;
the dedicated column was empty for every non-default row.
2. Manager list body now caps at content height (workspace count + 2
border rows + 1 sentinel row) instead of filling the whole frame.
5-6 workspaces no longer render in a box that looks two-thirds
empty.
* Revert "fix(launch): cap workspace list height to content"
The height cap made the space below the boxes visibly empty, which
reads worse than the previous full-height boxes. User feedback:
'before it was better when it was using the whole vertical space.'
Keeps the Agents tab header change from the same original commit
(3fdab9f3) — only the list-body sizing is reverted.
* fix(launch): hide FileBrowser .. entry at $HOME root
Previously the '../' entry was always shown in the file browser.
When the user was at $HOME, selecting it would escape the sandbox
(and was then clamped back by set_cwd) — confusing and cluttered.
Now the filter hides '..' when its target path is outside the root
subtree. At $HOME the entry disappears; at any subfolder of $HOME
it still appears so the user can navigate back up.
* feat(launch): hide empty right pane, split details, clickable git links
Three UX improvements:
1. When the cursor is on '+ New workspace' in the manager list,
the right details pane is hidden entirely — the list takes full
width. No more empty bordered box for the sentinel row.
2. Details pane split into three stacked sub-panels: General (workdir
+ last used), Mounts (tabular with header row), Agents (list or
'any agent'). Each has its own bordered mini-block with phosphor-
dark border and white-bold title. The outer 'Details' block is gone.
3. Git branch URL resolution wired up: inspect() now parses
<git_dir>/config to find the origin remote and derives a web URL
(GitHub, GitLab, generic HTTPS/SSH). MountKind::Git gains a
web_url: Option<String> field; MountKind::labeled_hyperlink() wraps
the branch name in OSC 8 escape sequences for supported terminals
(iTerm2, kitty, WezTerm, Alacritty, modern Terminal.app).
OSC 8 fallback: ratatui's Paragraph widget strips raw ESC bytes, so
the render path continues to call label() (plain text). The
hyperlink infrastructure (labeled_hyperlink, osc8_link, web_url) is
retained for a future raw-terminal-write path. Both are annotated
#[allow(dead_code)] with an explanatory TODO.
5 new unit tests on remote-URL parsing (GitHub SSH, GitHub HTTPS,
ssh:// protocol, GitLab SSH, config-file parse). All 566 tests pass.
* fix(launch): polish FileBrowser, WorkdirPick, and mount-dst prompt
- FileBrowser entries now render white instead of phosphor-green so the
bright-green highlight is the unambiguous focus indicator.
- TextInput prompts for mount destination say "destination (default:
same as host path)" instead of the internal "Mount dst" phrasing.
- WorkdirPick lines are laid out as a table: the path column is padded
to the widest choice so the dim+italic label column (`(mount dst)`,
`(parent)`, `(root)`, `(home)`) lines up cleanly.
- WorkdirPick filters `/` and the literal parent of `$HOME` (e.g.
`/Users` on macOS, `/home` on Linux) from the choice list — those
paths are never useful workdir targets.
- When a path is exactly `$HOME`, label it `(home)` instead of
`(parent)` so the workspace operator sees a recognisable name.
* fix(launch): FileBrowser s commits highlighted folder
Previously `s` always committed the explorer's cwd, which meant the
operator had to press Enter to navigate into the target folder before
committing — even though the folder was already highlighted and the
target of a single Enter press.
Reading `FileExplorer::current()` lets us commit the highlighted entry
directly when it is a real child directory. The synthetic `../`
parent-link row and the empty-listing case both fall back to the cwd,
preserving the previous behaviour for those edge cases.
The existing $HOME and `~/.jackin/*` rejection rules apply to whichever
path is chosen as the commit target.
* fix(launch): keep General-tab labels white; note agent-hyperlink TODO
- render_editor_row and render_editor_readonly_row no longer shift the
label column to phosphor-green when the row is focused. Labels stay
white (bold when focused); values keep their phosphor colouring for
editable rows and dim phosphor for read-only rows.
- Read-only rows used to render everything in phosphor-dim, which made
the editor view look washed-out. They now match the editable-row
label treatment (white) with a dim value + italic "(read-only)"
suffix, giving the operator a cleaner signal-to-noise ratio.
- Added a TODO in render_agents_subpanel mirroring the existing
labeled_hyperlink() note in render_mounts_subpanel: ratatui's
Paragraph strips OSC 8 ESC sequences, so agent-name → GitHub links
stay plain-text until a raw-write path exists.
* feat(launch): gate editor save on mount-collapse plan
The editor used to write configs straight to disk via
`ConfigEditor::edit_workspace` (or `create_workspace`), which meant the
operator could save a workspace with overlapping mounts like
`~/Projects` and `~/Projects/test`. The CLI rejects this unless you
confirm or pass `--prune`; the TUI now does the same.
Flow:
- On `s`, run `workspace::planner::plan_edit` (Edit) or `plan_create`
(Create) against the pending mount set.
- `CollapseError::{ReadonlyMismatch, ChildUnderExistingParent}` ->
error banner, no write.
- Pre-existing collapses only (no edit-driven) -> error banner
referencing `jackin workspace prune <name>`. The operator can't fix
these from the editor alone and the CLI prune command already exists
for this case.
- Edit-driven collapses -> open a `Modal::Confirm` with a
`ConfirmTarget::SaveCollapse` target, listing each child/parent pair
in the same wording as the CLI. On Yes, the save re-enters with
`EditorState::collapse_approved = true` and commits the collapsed
mount set via `plan.effective_removals` / `plan.final_mounts`. On No
/ Esc, pending mounts are kept intact so the operator can edit by
hand.
Pattern: a boolean flag on `EditorState` + a new `ExitIntent::RetrySave`
variant so the confirm-yes path reuses the existing modal-exit routing
but stays in the editor on success (rather than bouncing to the
workspace list, which is what `ExitIntent::Save` does). The plan
itself is not stashed; it is cheap to recompute on re-entry.
The `Confirm` widget now grows its prompt region to match the number
of lines in `state.prompt`, and `render_modal` sizes the outer rect
via `confirm::required_height` so multi-line collapse summaries render
without clipping.
Tests (5 new):
- `save_editor_opens_confirm_on_edit_driven_collapse`
- `confirming_collapse_writes_collapsed_set`
- `cancelling_collapse_keeps_pending_mounts_intact`
- `readonly_mismatch_produces_error_banner_no_write`
- `pre_existing_collapse_produces_prune_error_banner`
* feat(launch): structured footer with per-item styling
Introduce a `FooterItem` enum (Key / Text / Dyn / Sep / GroupSep) and a
shared `render_footer` that emits spans with a consistent palette:
- Key glyphs (↑↓, Enter, e/n/d/q, Tab, Esc, S, Y/N, *, Space) render in
WHITE + BOLD so they pop out of the legend.
- Action labels ("launch", "edit", "new", …) render in PHOSPHOR_GREEN.
- Inline dots (·) render in PHOSPHOR_DARK as a faint separator.
- A GroupSep (three spaces, no style) introduces a wider visual gap
between logical groups — navigation, per-row actions, and exit.
Migrate every footer call site to this scheme:
- `manager/render.rs` List / CreatePrelude / ConfirmDelete / Editor
footers build `Vec<FooterItem>` explicitly so the grouping is
deliberate per stage.
- Agent-screen footer in `launch/render.rs` uses the same inline spans.
- Modal-local hints inherit the scheme (file_browser navigation + "[S]
to use this folder" affordance, text_input "Enter confirm · Esc
cancel", confirm "Tab cycle · Enter confirm · Y yes · N no", and
save_discard).
Add unit tests covering the span-style mapping per variant plus
smoke tests for the List and ConfirmDelete stage footers.
* fix(launch): keep right pane visible on '+ New workspace' sentinel
Batch 7 expanded the list to full width when the cursor landed on the
sentinel row. The operator wants the 45/55 split preserved — the layout
should not shift as the cursor moves — with the right pane rendered as
an empty bordered block (same PHOSPHOR_DARK border as the General /
Mounts / Agents sub-panels) when there is no workspace to describe.
* fix(launch): align mount-table header with data columns
The mount-table header was a hardcoded string (" path<23 spaces>mode<3
spaces>type") while data rows computed their path column width
dynamically from the widest row. When paths were shorter than 23 chars
the header appeared drifted relative to the data; when they were longer
the header's "mode" column collided with the data's mode column at a
different offset.
Share the column-width computation between the header and data rows:
- Extract `mount_path_width` which returns max(row_path, "path".len(),
10) so the header and data always use the same column boundary.
- Add `render_mount_header(path_w)` that uses the same format string as
the data rows, then have both the read-only details subpanel and the
editor Mounts tab consume it.
- Pin the `mode` column to a shared `MOUNT_MODE_COL_WIDTH = 4` constant
(covering "mode" as well as "rw"/"ro" + trailing space) so it no
longer over-pads inconsistently.
Add unit tests that build mount rows with mixed path lengths and assert
the header's "mode" column starts at the same character index as each
data row's "mode" column.
* feat(launch): describe workspace concept on '+ New workspace' pane
Replace the empty bordered block shown to the right of the manager list
when the sentinel row is focused with a two-panel description pulled
from the "What is a workspace?" / "Why save a workspace?" sections of
the workspaces guide. Keeps the right-hand real estate useful for
first-time operators and matches the General/Mounts/Agents sub-panel
chrome for visual consistency.
* feat(launch): restore 'Current directory' row in workspace manager
Before the TUI redesign the launcher's first row was a synthetic
"Current directory" choice that let operators launch an agent against
cwd without saving a workspace. The manager's rewrite dropped it; this
reinstates it as row 0 of the list with the right-pane summary, the
cwd-aware preselect, and the launch wiring that matches the old
behaviour.
Row layout (enforced by ManagerState::from_config, render_list_body,
and handle_list_key):
row 0 → synthetic "Current directory"
rows 1..=N → saved workspaces
row N+1 → "+ New workspace" sentinel
Edit (`e`) and Delete (`d`) are rejected on row 0 with a toast. Enter
on row 0 emits a new InputOutcome::LaunchCurrentDir; the run-loop
routes it through the same agent-picker transition as LaunchNamed,
reusing LaunchState::workspaces[0] (the CurrentDir choice built by
LaunchState::new). Preselect reuses find_saved_workspace_for_cwd so
TUI and CLI agree on "which workspace am I in?".
The right pane branches on row 0 → render_current_dir_details_pane
(dedicated renderer; no last-used row, no edit affordance, "any
agent"). The sentinel description pane lands in the same commit's
sibling already; saved-workspace rows continue to use the shared
render_details_pane with `workspaces[selected - 1]`.
Tests added:
- manager_preselects_saved_workspace_matching_cwd
- manager_preselects_current_directory_when_no_saved_matches
- manager_current_directory_is_first_row
- current_directory_row_rejects_edit_and_delete
- enter_on_current_directory_returns_launch_current_dir
* fix(launch): polish mount-header gap, modal titles, and FileBrowser size
- Mount table header: add two-space gutter between `mode` and `type`
so the header no longer reads "modetype". Data rows now emit the
matching two-space gap so the `type` column aligns in both the
read-only Mounts subpanel and the editor Mounts tab.
- Text-input + Workdir-pick modal block titles render WHITE + BOLD to
match the General/Mounts/Agents block titles on the main screen.
Confirm + SaveDiscard already use WHITE+BOLD — left untouched.
- WorkdirPick path values render WHITE (the `(mount dst)`/`(parent)`/
`(home)`/`(root)` label suffix stays PHOSPHOR_DIM italic).
- FileBrowser modal height drops from 70 absolute rows to 22 so it
no longer eats the whole screen. Width stays at 70%.
* feat(launch): classify git mounts by host and relabel GitHub remotes
- Introduce `GitHost { Github, Other }` on `MountKind::Git` so the
render path can tell which remotes have an "open in browser"
affordance. `inspect` populates this from `parse_remote_origin_url`:
SSH `git@github.com:`, HTTPS `https://github.com/…`, and
`ssh://git@github.com/…` all resolve to `Github`; anything else
(self-hosted, GitLab, no remote, unparseable URL) falls through to
`Other`.
- `remote_to_web` now returns `Some(url)` only for GitHub hosts and
`None` for everything else — it no longer synthesises `gitlab.com`
URLs. Non-GitHub remotes keep `web_url: None` on the `MountKind`.
- `MountKind::label()` renders `github · {b}` / `github · detached {sha}`
/ `github` for GitHub hosts and keeps the generic `git · …` prefix
for `Other`. `MountKind::Folder` / `Missing` unchanged.
- `remote_to_web_gitlab` test re-purposed to assert GitLab (and other
non-GitHub hosts) now return `None`. New tests for the GitHost split
via `inspect` and for the `remote_points_at_github` predicate covering
all three URL forms + a GitHub-lookalike subdomain rejection.
* feat(launch): 'o' key opens highlighted GitHub mount in the browser
- Add the `open` crate (5.x) so the editor can launch the system
browser without blocking the TUI (`open::that_detached`).
- Wire `o` into the editor's Mounts tab: when the cursor is on a
mount row whose source resolves to a GitHub-hosted repo with a
web URL, pressing `o` opens that URL in the operator's default
browser. Non-GitHub / folder / missing mounts emit an "no GitHub
URL for this mount" toast so the hint is discoverable; the sentinel
"+ Add mount" row is a silent no-op.
- `contextual_row_items` now composes an `o open in GitHub` item
onto the existing `d remove · a add` pair when the current row is
a GitHub mount. List-view mounts pane is unchanged — the `o` key
only binds in the editor.
- Tests: `github_mount_row_includes_open_in_github_hint` and
`non_github_mount_row_omits_open_in_github_hint` pin the footer
composition. No unit test for the browser side-effect itself.
* feat(launch): flag git repos in FileBrowser and offer mount-or-dive prompt
Part A — directory listing:
- `annotate_file` (new filter_map body) stats each directory for a
`.git` child and appends U+25A3 (▣) to the display name when present.
Works for plain clones (`.git` is a directory) and submodules (`.git`
is a file containing `gitdir: …`). Single stat per entry — no
recursive walk.
Part B — Enter on a git-repo row:
- New `GitPromptFocus { MountHere, EnterIn, Cancel }` + two fields on
`FileBrowserState` (`pending_git_prompt`, `pending_git_focus`) drive
an in-widget confirm overlay. Enter on a git-repo row opens the
prompt; Tab/←→/h/l cycle focus; Enter commits the focused option;
M/E/C are direct shortcuts; Esc dismisses the prompt without
cancelling the browser.
- MountHere commits the repo path through the same sandbox rules as
`s` (rejects root / `~/.jackin/*`). EnterIn navigates into the repo
via `explorer.set_cwd` (avoids re-posting Enter, which would re-open
the prompt). Cancel just clears state. Non-git folders keep their
usual Enter-navigates-in behavior.
- Overlay renders as a centred 3-button bar inside the explorer area
so the listing stays visible as context. Phosphor palette + focus
styling mirrors `confirm.rs`/`save_discard.rs`; button ring copied
locally rather than cross-importing between widgets.
- Footer legend swaps to `Tab cycle · Enter confirm · Esc cancel`
while the prompt is active.
Tests cover: marker on `.git`-dir and submodule-`.git`-file cases, no
marker on plain folder, Enter opens prompt on repo row, MountHere
commits the path, EnterIn navigates in and clears prompt, Cancel
clears without cwd change, Esc dismisses prompt without cancelling
browser, plain-folder Enter navigates as before, and the `M` shortcut
commits regardless of current focus.
* feat(launch): add mount-destination choice modal widget
Introduces the `mount_dst_choice` widget and wires a new
`Modal::MountDstChoice` variant into the manager's modal enum plus
render dispatcher. No input behaviour changes yet — follow-up commits
swap the Editor and Prelude FileBrowser→TextInput chains to route
through this modal.
The widget is the 3-button focus-ring pattern pioneered by
`save_discard`: default focus on `OK`, Tab/BackTab cycling, and single-
letter shortcuts (`o`/`e`/`c`). Default on `OK` because the common case
is `dst = src`, so an accidental Enter commits that without surprise.
* feat(launch): route editor add-mount through destination choice modal
The FileBrowser→TextInput chain in the Editor's Mounts tab assumed
every operator wanted to edit the destination path. In practice, 95%
of mounts commit with dst = src. Swapping in the new MountDstChoice
modal makes the common path a single Enter press and keeps the old
behaviour one keystroke away via `Edit destination`.
`apply_file_browser_to_editor` now opens MountDstChoice instead of
pushing a provisional mount plus TextInput. The actual push happens
in the MountDstChoice commit handler:
- OK: push MountConfig { src, dst = src, rw }, close modal.
- Edit destination: push the provisional mount (as today) and open
Modal::TextInput{MountDst} pre-filled with src. The existing
TextInputTarget::MountDst handler overwrites the provisional dst.
- Cancel / Esc: close the modal, leave pending.mounts untouched.
Behavioral tests pin all three paths and guarantee no mount is
pushed until the operator commits in the choice modal.
* feat(launch): route create-prelude add-mount through destination choice
Mirrors the editor-side change: the Create wizard's FileBrowser step no
longer assumes the operator wants to edit the destination. Instead, the
prelude now opens MountDstChoice after FileBrowser commits, offering
the fast `OK` path that skips TextInput entirely.
Both paths (OK and TextInputDst commit) share a new helper
`prelude_advance_to_workdir_pick` so the downstream WorkdirPick stage
receives the same staged mount regardless of whether the operator
edited the destination. This keeps the chain FileBrowser → (choice) →
WorkdirPick → TextInput(Name) intact for the `OK` shortcut.
Cancel on MountDstChoice matches today's Esc-during-TextInput
behaviour: close the modal, leave the prelude state alone so the
outer dispatcher treats it as a wizard-cancellation and returns to
the manager list.
* refactor(launch): extract mount-dst-choice dispatch to keep clippy happy
The inline `Modal::MountDstChoice` arm inside `handle_editor_modal`
pushed the function above clippy's 100-line ceiling. Extract the
outcome dispatch into `dispatch_editor_mount_dst_choice` and tidy the
helper's doc comment so `TextInput` doesn't trip the missing-backticks
lint. No behavioural change.
* fix(launch): strip trailing slash in FileBrowser name filter
ratatui-explorer appends `/` to directory names at runtime, so the
filter in `annotate_file` was comparing `"Library"` against `"Library/"`
and silently letting every excluded entry render. Same bug let `..`
through on the sandbox-escape check. Normalize with
`trim_end_matches('/')` before matching, and harden the `s`/Enter
paths + default key dispatch to guard against empty listings (the
fixed filter can now produce an empty `files()` which made
`current()` and nav-key dispatch panic inside ratatui-explorer).
* polish(launch): simplify Destination modal title
Rename the mount-destination TextInput label from
`destination (default: same as host path)` to plain `Destination`.
The parenthetical hint is redundant after batch 12: the TextInput
only opens when the operator explicitly picks "Edit destination"
on the MountDstChoice modal, so they're already in deliberate-edit
mode with the src pre-filled as the default.
Also capitalizes the title to match the other modal block titles
(Confirm, Unsaved changes, Mount destination, Git repo detected,
Workdir pick, Rename workspace, Name this workspace). No other
titles needed changes — the audit was clean.
* feat(launch): bind Left/Right to prev/next tab in Editor
Extend `handle_editor_key` so Right matches Tab (forward cycle) and
Left matches BackTab (reverse cycle). Wrap-around behavior mirrors
the existing Tab contract: General → Mounts → Agents → Secrets →
General, and symmetrically for reverse.
Modal-open precedence is already guarded by the early-return in
`handle_key` — Left/Right continue to feed into modal handlers
(Confirm, SaveDiscard, MountDstChoice) when a modal is active.
* feat(launch): wire list-view `o` to open workspace GitHub mounts
Extend the `o` key beyond the Editor's Mounts tab to the workspace
list view. On a saved workspace row:
0 GitHub mounts → toast "no GitHub URLs for this workspace"
1 GitHub mount → open::that_detached immediately
≥2 GitHub mounts → open a new GithubPicker modal; Enter commits
the highlighted URL to open::that_detached.
Row 0 (Current directory) and the `+ New workspace` sentinel toast
`no workspace selected` for discoverability. The list footer now
surfaces `o open in GitHub` only on rows whose workspace resolves
to ≥1 GitHub-hosted mount.
Adds:
- new `widgets/github_picker.rs` widget (title-styling and tab-list
pattern mirror WorkdirPick so the modal feels native);
- `Modal::GithubPicker { state }` variant, with arms closed in the
render-size switch, `handle_editor_modal` (defensive cancel), and
a new `handle_list_modal` dispatcher;
- `list_modal: Option<Modal<'a>>` slot on ManagerState — list-view
modals weren't previously anchored anywhere; Editor/CreatePrelude
keep their per-stage modal slots unchanged;
- `resolve_github_mounts_for_workspace` helper, shared by the input
handler and the render-side footer-hint guard.
Piggybacks a one-line clippy fix in file_browser.rs
(`iter().any(|x| *x == bare)` → `EXCLUDED.contains(&bare)`) that
surfaced after the trailing-slash filter landed.
* fix(launch): drop cwd suffix from Current directory row label
The list row for the synthetic "Current directory" choice used to read
`Current directory (~/Projects/foo)`. The right-pane details already
show the cwd on the `workdir` line, so the parenthetical suffix is
duplicate visual load. Render just `Current directory`.
Row 0 keeps its WHITE colour so the synthetic choice still visually
separates from the phosphor-green saved workspaces below it.
* fix(launch): align mount-table type column with header
MOUNT_MODE_COL_WIDTH was 2, matching the literal width of rw/ro but
leaving a 2-char gap before the data row's kind column versus the
header's 4-char "mode" label. Header and data rows shared the same
"{mode:<mw} type" format string but MOUNT_MODE_COL_WIDTH no longer
matched the header label length, so `type` and its data (e.g. "folder")
rendered at different offsets.
Pin MOUNT_MODE_COL_WIDTH to 4 so rw/ro pad to the header's "mode"
width. Both the header and data emit the same two-space gutter before
the `type` column, so the kind label lines up with the header offset.
Extend the existing gap-between-mode-and-type test to additionally
assert that `header.find("type") == data.find("folder")` — the
type-column offset must match for a row with a plain folder mount.
* refactor(launch): drop dead OSC 8 hyperlink scaffolding
The `o`-opens-in-system-browser path (via open::that_detached)
supplanted the aspirational OSC 8 hyperlinks-in-terminal route. The
OSC 8 helpers were already #[allow(dead_code)] — remove them:
- `osc8_link()` — wrapped text in OSC 8 ESC sequences;
- `MountKind::labeled_hyperlink()` — built a GitHub-linked label
from a branch/sha + url;
- their associated NOTE blocks on render.rs (mounts subpanel) and
the agents-hyperlink TODO next to render_agents_subpanel.
The `web_url: Option<String>` field stays — the `o` key consumes
it to open the branch URL. Likewise `remote_to_web`,
`parse_remote_origin_url`, and the `GitHost::Github` classification
are all still in the live path.
Clippy baseline (4) unchanged; no tests touched.
* feat(launch): mouse-draggable list/details divider
Add a draggable seam between the workspace list pane and the details
pane in the manager TUI. Click-and-drag on the seam column (within ±1)
resizes the split; the percentage is clamped to [20, 80] so neither
pane can be starved.
Mechanically:
- ManagerState gains `list_split_pct: u16` (default 45) and
`drag_state: Option<DragState>`. `clamp_split` + split-range consts
live alongside. `render_list_body` reads `list_split_pct` instead
of the hard-coded 45/55.
- `src/launch/mod.rs` enables `EnableMouseCapture` after entering the
alternate screen and `DisableMouseCapture` in the terminal guard's
Drop. Side-effect: the terminal's native click-drag text selection
stops working while the TUI is running — hold Shift (Terminal.app,
iTerm2) or Option (iTerm2) to bypass. Documented inline.
- The run-loop now matches on `Event::{Key, Mouse, _}` (was a bare
`if let Event::Key`). Mouse events in the Manager stage dispatch
to a new `manager::input::handle_mouse` with the current terminal
size as a `ratatui::layout::Rect`.
- `handle_mouse` hit-tests the seam, captures a `DragState` anchor
on `Down(Left)`, updates `list_split_pct` on `Drag(Left)`, and
clears the anchor on `Up(Left)`. It also gates on List stage, no
open list-modal, and `term_size.width >= 40`.
Unit tests (8 new, pure state manipulation — no ratatui loop):
- mouse_down_on_seam_starts_drag
- mouse_drag_updates_split_pct
- mouse_drag_clamps_to_min_and_max
- mouse_up_ends_drag
- mouse_down_far_from_seam_does_not_start_drag
- drag_ignored_when_list_modal_open
- drag_ignored_on_non_list_stage
- drag_ignored_when_terminal_too_narrow
Clippy baseline (4) unchanged.
* fix(launch): rename current-dir pane first block to "General"
The synthetic "Current directory" row has a right-pane first block titled
" Current directory ", but the left-list row label already conveys that
context. Rename to " General " to match the saved-workspace details pane
(General / Mounts / Agents) so both panes use the same three sub-panel
titles.
* fix(launch): remove phantom empty row in current-dir Mounts block
`render_current_dir_details_pane` hard-coded the Mounts block at
`Constraint::Length(5)`, which over-allocated by one row for the
single-mount current-directory case and left a visible empty line
inside the block border.
Extract the height formula from `render_details_pane` into a shared
`mount_block_height` helper (2 borders + 1 header + max(1, N) data rows,
clamped to 12) and use it from both pane renderers so the two paths
produce identically-tight Mounts blocks.
Covered by four regression tests pinning the formula for the empty,
single, multi, and many-mount cases.
* fix(launch): align General sub-panel content with Mounts/Agents
The General sub-panel (on both the saved-workspace pane and the
current-directory pane) rendered its `workdir`/`last` rows flush against
the block's left border, while the Mounts and Agents sub-panels already
used a two-space indent. The mismatch gave the right pane a jagged left
edge across the three stacked blocks.
Add the same two-space prefix to the General rows on both panes. The
convention is pinned by a new `SUBPANEL_CONTENT_INDENT` constant and
two visual regression tests that render each sub-panel to a
`TestBackend` buffer and assert the first visible character of row 0
sits at that indent relative to the block's left border. Covers the
"any agent" fallback and the starred-default-agent row explicitly.
* fix(launch): follow worktree commondir for GitHost detection
Git worktrees are checkouts whose `.git` is a file pointing at
`<main>/.git/worktrees/<name>`. That per-worktree gitdir owns HEAD but
has no `config` of its own — the shared config (including the remote
URL) lives at the target of a `commondir` pointer. The previous
`resolve_git_dir` stopped at the worktree-specific gitdir, so
`resolve_host_and_url` read nothing, `GitHost` defaulted to `Other`,
and the label rendered as `git · branch` instead of `github · branch`
for every worktree of a GitHub-hosted repo.
Split resolution into `resolve_gitdirs`, which returns a pair:
- `work_dir` — owns HEAD (worktree-specific for worktrees, identical
to `config_dir` for plain clones and submodules).
- `config_dir` — owns the remote URL (follows `commondir` when present,
handling both relative and absolute pointer forms).
`inspect` now parses HEAD from `work_dir` and the remote URL from
`config_dir`, so the host is re-classified correctly for worktrees
without perturbing the submodule path.
Covered by three new tests:
- `worktree_gitfile_resolves_to_commondir` (relative commondir)
- `worktree_commondir_with_absolute_path`
- `submodule_gitfile_still_resolves_host_end_to_end` (regression guard
for submodules — HEAD + config co-located, no commondir)
* refactor(launch): drop FileBrowser affordance banner
The "press [S] to use this folder" banner above the explorer is redundant
with the `S select` footer hint below it, and inconsistent with other
modal styling (no other modal has a top banner). Drop it and its layout
constraint so the explorer shifts up by one row.
The `rejected_reason` banner (shown when the operator picks $HOME itself
or a `.jackin/` path) stays — it is functional error feedback, not an
affordance hint.
* refactor(launch): prefix git marker, "repo" -> "repository" in UI
Three small FileBrowser/git-prompt polish items:
- Prepend the git-repo marker instead of appending. Changed the glyph
from U+25A3 (white square with black small square) to U+2387
(alternative key symbol — reads as a branch) and moved it to the head
of the directory name so the eye lands on it first. The trailing
marker was easy to miss; a file listing now shows
"⎇ scentbird-root/" rather than "scentbird-root/ ▣".
Noted as a one-liner in the code: per-entry colouring would need
dropping ratatui-explorer — out of scope here.
- Rename user-facing "repo" to "repository". The button label becomes
"Mount this repository" and the prompt title becomes
"Git repository detected". Identifiers (repo_dir, GIT_REPO_MARKER,
test fixture names) are left alone — this is a UI-string change only.
- Rename the middle git-prompt button from "Enter to pick subdirectory"
to "Pick a subdirectory" — imperative voice parallel to the first
button, no more "key + verb" mix. Footer hints under the prompt
still read "E enter" for the shortcut, which remains correct.
* refactor(launch): uppercase single-letter hotkeys in footer hints
Operator prefers the `M mount · E enter · C/Esc cancel` style
consistently across the TUI. Previously the list-view footer was
lowercase (`e edit · n new · d delete · o open in GitHub · q quit`)
while the git-prompt hint was uppercase. Normalise every footer site
on uppercase single-letter keys; multi-character glyphs (Enter, Tab,
Esc, ↑↓, etc.) and non-alpha keys (`*`) pass through unchanged.
Updates:
- `src/launch/manager/render.rs` list footer: E/N/D/O/Q
- `src/launch/manager/render.rs` editor save footer: S
- `src/launch/manager/render.rs` contextual_row_items: D/A/O on
Mounts rows, A on the "+ Add mount" sentinel
- `src/launch/widgets/file_browser.rs` nav hint: S select,
H/← up
- Existing footer test assertions updated to match new casing
- New `footer_hotkeys_are_uppercase` test scans contextual hints
(Mounts row + sentinel, Agents) and verifies every single-char
alphabetic `Key` item is uppercase
Key handlers extended to accept both cases where a footer now shows
uppercase. Most handlers already matched `'e' | 'E'` from batch 11;
the remaining lowercase-only sites (list Q/E/N/D/O, list K/J nav,
editor S/K/J, editor-Mounts A/D/O, file_browser S/H/L nav, picker
J/K) now take `'x' | 'X'`. Behavioural change is nil — Caps Lock
and Shift-held hotkeys now work where they already did at the
footer-advertised case.
* feat(launch): click-to-select in workspace list
Extend `handle_mouse` with row-click selection on the workspace-list
pane. Left-button Down inside the list content area maps to the row
index and updates `ms.selected`; clicks on the seam column still
start a drag (regression guard for batch 14).
Hit-test rules:
- Seam always wins — a click within ±1 column of the current seam
starts a drag regardless of y. This keeps the resize affordance
unambiguous even when the seam overlaps a valid row position.
- Otherwise, clicks inside `[1, seam - 1]` × `[header + 1, body_end - 1]`
(left-pane interior minus borders) convert to a row index via
`mouse.row - (header_height + 1)`.
- The index must be in `[0, sentinel_idx]`; beyond that we silently
drop the click. Row 0 = "Current directory", 1..=N = saved, N+1 =
"+ New workspace" sentinel.
- Clicks outside those ranges (header, footer, borders, right pane)
are ignored.
Layout heights are pulled from two new private consts mirroring
`render::render`'s `Constraint::Length(3)`/`Length(2)`. If the
chrome ever changes shape, both the render and hit-test paths need
updating together.
Double-click = launch is intentionally skipped: crossterm doesn't
emit native double-click events, so implementing it would need
tracking `(last_row, last_instant)` on `ManagerState` with a debounce
window. That's more state-machine than one item in this batch
warrants — left as a follow-up. Single-click-to-select is the
must-have and is fully wired.
Five new tests cover the happy path (row 0, mid-list row, sentinel
row), the negative path (header / borders / right pane / below
sentinel / footer), and the seam precedence regression guard.
* fix(launch): FileBrowser Esc steps back one level when not at root
Previously Esc always cancelled the modal. If the operator drilled into
a subfolder (via Enter on a plain dir or via the git-prompt
"Pick a subdirectory" path), a stray Esc collapsed the whole picker and
returned to the workspace list. Operators expect Esc to back out one
level — mirroring the behavior of `h` / `←` — and only cancel when
already at root.
Esc now:
- Clears any stale rejected_reason.
- Navigates one level up when cwd != root (sandbox-guarded, same as the
existing root-clamp).
- Cancels the modal only when cwd == root.
Git-prompt Esc is unchanged: it still dismisses only the prompt and
leaves the explorer open at the current cwd.
Footer hint updated from "Esc cancel" to "Esc up/cancel" (matching the
batch 16 uppercase-hotkey convention) — accurate for both drilled-in
and root cases.
Five new tests cover: esc-at-root cancels, esc-in-subfolder navigates
up, esc-three-levels-deep goes up exactly one, esc clears
rejected_reason, and the git-prompt Esc regression guard.
* refactor(launch): default workspace list/details split 45 -> 30
Workspace names like `chainargos-blockchain-nodes` fit comfortably at
30%; the details pane gets the breathing room for git branches and
full paths. MIN/MAX bounds (20/80) stay unchanged.
Also refactor the seam drag-and-select tests in `input.rs` to refer to
`DEFAULT_SPLIT_PCT` instead of the literal `45`, so future changes to
the default don't silently break the test assumptions about the seam
column.
* refactor(launch): agents subpanel moves default-marker star to trailing position
The read-only Agents sub-panel in the list/details pane previously placed
a leading star prefix on the default-agent row — "★ alpha" — so default
rows rendered at col 4 while non-default rows rendered at col 2. Move the
star to a trailing position after the name, separated by a space, so
every agent name starts at `SUBPANEL_CONTENT_INDENT` (col 2) matching
the General and Mounts sub-panels' leading-indent convention.
The star renders as its own `Span` styled with `PHOSPHOR_DIM` — keeps
the agent-name base color (`PHOSPHOR_GREEN` when registered globally,
`PHOSPHOR_DIM` when not) untouched and keeps the marker low-chrome.
The Editor tab's Agents view (`render_agents_tab`) is intentionally
untouched — that layout carries `[x]`/`[ ]` toggles with a different
selection affordance and is a separate concern.
Replaces the legacy `agent_star_row_aligns_with_content_column` test
with three focused tests covering name alignment on non-default rows,
trailing-star position on default rows, and name alignment on default
rows (independent of the trailing star).
* feat(launch): show origin URL in FileBrowser git-repo prompt
Enter on a git-repo folder now opens the "Git repository detected"
overlay with the origin web URL displayed between the title and the
question. The URL is resolved via `mount_info::inspect` when the
prompt opens and cleared when the prompt is dismissed, so stale URLs
don't leak between repos.
Non-GitHub remotes (and repos with no origin) resolve to `None` and
the URL row is elided entirely — no "(no URL)" noise. The overlay
height grows by one row only when a URL is present.
* feat(launch): Esc rewinds the create-workspace wizard one step
Esc at any step of the create-workspace prelude now steps back to
the previous step instead of abandoning the whole wizard:
- FileBrowserSrc (step 1) → workspace list (no prior state)
- MountDstChoice → reopen FileBrowserSrc at the last cwd
- TextInputDst → reopen MountDstChoice
- WorkdirPick → reopen MountDstChoice or TextInputDst
depending on which dst-path was taken
- TextInputName → reopen WorkdirPick
`CreatePreludeState` grows two breadcrumb fields (`last_browser_cwd`,
`used_edit_dst`). `FileBrowserState` exposes `cwd()` / `set_cwd()`
so the wizard can restore the exact directory the operator was
browsing when src was committed — no more starting back at $HOME.
* refactor(launch): replace ratatui-explorer with a custom file browser
ratatui-explorer 0.3's Theme exposes only a single `dir_style` shared
by every directory row — meaning the operator affordance "highlight
git repos differently" was impossible without a rewrite. The custom
browser renders each row directly via tui-widget-list + Paragraph, so
git rows can carry a distinct trailing ` (git)` suffix in phosphor-dim
bold while plain folders stay plain phosphor green.
Other gains vs the ratatui-explorer wrapper:
- `h/l`, arrows, `s`, `Esc`, and Enter are handled directly — no more
round-tripping through `FileExplorer::handle` with div-by-zero guards
on empty listings.
- `cwd()` / `set_cwd()` are first-class methods, used by the create-
workspace wizard's step-back navigation.
- `FolderEntry { is_git }` replaces the fragile U+2387 prefix hack.
- The EXCLUDED-at-root / sandbox / $HOME-clamp / `~/.jackin` rejection
behaviour is preserved exactly.
The ratatui-explorer dependency is dropped. Every existing FileBrowser
test was ported; three new tests cover per-entry is_git tagging and
the rendered ` (git)` suffix.
* style(launch): space-pad modal block titles
TextInput and WorkdirPick titles previously rendered flush to the
top-left border (`┌Name this workspace...`). Wrap them in leading
and trailing spaces so every modal renders with the canonical
`┌ Title ─...` padding, matching SaveDiscardCancel / Confirm /
MountDstChoice / GithubPicker / FileBrowser.
* refactor(launch): rename workdir to working dir in UI strings
User-facing occurrences of "workdir" now read as "working dir" (in
narrow label columns where column alignment matters) or "Working
directory" (in modal titles and contextual footer hints). Struct
field names, method names, variable names, test identifiers, and
other non-UI uses keep the shorter "workdir" spelling.
Touched:
- WorkdirPick modal title becomes "Working directory — pick from mounts".
- General sub-panel label becomes "working dir " (col-padded to 12 to
match a widened "last " column).
- General-tab editor rows use "working dir" (fits existing 15-wide
label padding).
- Footer hints become "Enter pick working directory".
- Launch preview sidebar label becomes "work dir " (matches the 9-wide
"agent " column in the same pane).
* style(launch): align every popup to the canonical modal template
Operator feedback: popups looked inconsistent and that made the whole
UI feel messy. Pull every modal onto one shared visual contract so a
future widget has an obvious shape to follow.
Canonical template (modelled on SaveDiscardCancel):
- Border: PHOSPHOR_DARK (0,80,18) — dim enough that the title + focus
highlight pop.
- Title: Title Case, wrapped in leading + trailing spaces, WHITE+BOLD.
- Hint row (PHOSPHOR_DIM separators between groups): WHITE+BOLD keys
and PHOSPHOR_GREEN labels, canonical verbs per modal kind.
- Choice buttons: focused = WHITE bg / Black fg / BOLD; unfocused =
PHOSPHOR_GREEN fg / BOLD (no bg) so only the focused choice pops.
4-space gap between buttons, 2-space padding inside each label.
Per-widget changes:
- save_discard: border green -> dark. Unfocused buttons drop the
phosphor bg. Sep variable uses phosphor_dark locally.
- confirm: border green -> dark. Unfocused Yes/No buttons drop the
phosphor bg. Footer styles re-use the local constants instead of
inlined RGB literals.
- mount_dst_choice: unfocused buttons drop the phosphor bg. Border
was already dark.
- text_input: border green -> dark. Hint moves inside the bordered
block so the bottom border is unbroken. Inner layout is top pad /
input / spacer / hint (canonical). Render-table bumps TextInput
height from 5 to 6.
- workdir_pick: border green -> dark. Adds blank top padding, blank
spacer, and a canonical navigate/confirm/cancel hint row (was
missing entirely).
- github_picker: border green -> dark. Same inner layout as
workdir_pick (blank / list / blank / hint). Height table adjusts
the `+4` chrome allowance to `+5` for the hint row.
- file_browser git-repo overlay: border green -> dark. Buttons drop
the phosphor bg. Key binding for "Pick a subdirectory" renames
from `e/E` to `p/P` so the hint (`M mount · P pick · C/Esc cancel`)
matches the button label. Hint gets the canonical `Tab cycle ·
Enter confirm` prefix. Overlay widens from 60 to 80 so the three
buttons + canonical hint line both fit on one line without wrapping.
New consistency tests in widgets/mod.rs pin the contract:
- all_modal_block_titles_have_padding
- all_modal_borders_are_phosphor_dark
- all_modal_hint_rows_use_canonical_styles
Each iterates every modal render and asserts the shared invariants.
* style(launch): strip 'Tab cycle' …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design spec for PR 2 of the launcher-workspace-manager series. Adds a workspace manager screen to the jackin launcher — list, create, edit, and delete workspaces without leaving the TUI.
Series: PR 1 of 3 — toml_edit migration (#162) · PR 2 of 3 — this spec · PR 3 of 3 — Environments tab + 1Password picker (#171)
Design decisions (all settled during brainstorming)
mkeypress. Today's launch path is untouched.s. Pending changes drive dirty markers; Esc-with-pending opens Discard/Save/Cancel.digital_rain(fromsrc/tui/animation.rs),step_shimmer,spin_wait, and landing-page color tokens fromdocs/src/components/landing/styles.css. One new area-bounded rain widget extracted fromanimation.rs.Three new reusable widgets —
TextInput,FileBrowser,Confirm— emerge from this work. PR 3's Environments tab consumes all three unchanged.Dependencies
toml_editmigration, merged in feat(config): toml_edit migration (PR 1 of 3) #162): all persisted writes flow throughConfigEditor.Non-goals
[docker.mounts]management — CLI only.jackin config workspace …CLI semantics — CLI remains source of truth.CHANGELOG.md— operator curates.Test plan
docs/superpowers/specs/2026-04-23-workspace-manager-tui-design.md🤖 Generated with Claude Code