feat(launch): workspace manager TUI (PR 2 of 3)#166
Merged
Conversation
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`is_within_root` canonicalizes the candidate for the containment check, but the two reserved-target rules (`target == self.root` and `target.starts_with(.jackin)`) compared against the lexical target. A symlink under $HOME whose canonical form IS $HOME itself — or IS inside ~/.jackin — passed those checks. Canonicalize the commit candidate once at function entry, use the canonical form for every policy rule, and return the lexical path for UI display. `.jackin` join is also canonicalized so a symlinked `.jackin` can't fool `starts_with`. Adds three cfg(unix) regression tests for the new policy: root-aliased symlink rejected, .jackin-aliased symlink rejected, plain-folder-aliased symlink accepted. Addresses finding #2 of the second-pass PR #166 review. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…d helper
CreatePreludeState flagged completion as "pending_name.is_some()",
but the dispatcher then called `build_workspace().expect("prelude
complete")` — the two invariants drifted independently.
Add `CreatePreludeState::completed() -> Option<(String,
WorkspaceConfig)>` that checks every required field together and
returns the owned pair the dispatcher needs. The `expect`/`unwrap`
in the production path goes away; the dispatcher now transitions
only on `Some`.
Addresses finding #5 of the first-pass review (carried into the
second-pass review as well).
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
The `input/mod.rs` `#[cfg(test)] mod tests` block held ~1800 lines of test cases covering every stage. That concentration forced future maintainers to navigate a large mixed-responsibility file and surfaced in static analysis as a cluster of duplicate-binding warnings localized to one place. Disperse the tests so each submodule owns the tests for the behavior it owns: - list-stage tests -> input/list.rs - editor-stage tests -> input/editor.rs - save-flow tests -> input/save.rs - create-wizard tests -> input/prelude.rs - mouse tests stay in input/mouse.rs input/mod.rs retains only the genuine cross-flow test that drives multiple stages in one case (create-mode rename + save). Shared helpers `key()` and `mount()` are lifted to a `test_support` sibling module since they show up in 3+ submodules; submodule- local helpers (setup_with_workspace, press_s, etc.) stay co- located with their tests. No behavior change. Test count unchanged (829 -> 829). Addresses finding #6 of the second-pass PR #166 review. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Mechanical cleanup of clippy warnings in files added or modified by PR #166. Addresses finding #4 of the second-pass review: tests/manager_flow.rs: - missing_const_for_fn on the `key` helper -> mark `const fn`. - redundant_clone on the third use of `host_path` -> drop the trailing `.clone()` so the value moves on its last use. - default_trait_access on `Default::default()` for env/agents fields -> use the explicit `BTreeMap::new()` constructor. src/launch/manager/input/save.rs (test module): - used_underscore_binding on `_tmp` -> rename to `tmp`. The binding has to be held to keep the TempDir alive for the test, but it is also read via `.path()`, so the underscore was misleading; the rename makes the read explicit. - used_underscore_binding on `_config0` -> swap for the `_` bit-bucket pattern; the loaded config is genuinely unused in those tests (they re-open the config via ConfigEditor). - redundant_clone on `ws_b.clone()` at the last use site -> drop the clone and let the value move. No #[allow(...)] suppressions -- every warning is addressed by code change. Warnings in files outside this PR remain as documented pre-existing drift (finding #9 of the first-pass review). Addresses finding #4 of the second-pass PR #166 review. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
The user-facing command is `jackin` / `jackin console`; `jackin launch`
is deprecated. The internal module name `src/launch/` was a relic of
the old CLI verb and forced future maintainers to translate between
the operator-facing "console" terminology and the source-tree's
"launch" terminology.
Rename the module wholesale:
- `src/launch/` -> `src/console/` (full directory rename via git mv).
- `crate::launch::` -> `crate::console::` in every importer
(src/app, tests/manager_flow.rs, and the renamed files themselves).
- `run_launch` -> `run_console` (entrypoint fn).
- `LaunchState` -> `ConsoleState`, `LaunchStage` -> `ConsoleStage`
(top-level state machine types).
- Internal doc-comments updated where they read "the launcher" /
"launch.rs" / "src/launch/mod.rs" - those become "the operator
console" / "console/" / "src/console/mod.rs".
- PROJECT_STRUCTURE.md row for the module updated to `console/` and
the `tui/` row's "separate from the launcher" prose updated to
"separate from the operator console".
Out of scope, intentionally untouched:
- `src/runtime/launch.rs` and its `for_launch` constructor - that file
is about agent-runtime launching (CLI surface `jackin load`), not the
operator console.
- `src/cli/dispatch.rs::LAUNCH_DEPRECATION_WARNING`,
`LaunchArgs = ConsoleArgs` alias, `Command::Launch` - explicitly about
the deprecated CLI verb, behavior and string content stay.
- `docs/src/content/docs/commands/launch.mdx` and the deprecation
`<Aside>` in `commands/console.mdx` - deprecation surface, unchanged.
- `manager::InputOutcome::LaunchNamed` / `LaunchCurrentDir` - intent
verbs ("user requested a workspace launch action"), not subsystem
names; renaming would be cosmetic churn.
- `git_prompt.rs` "when the launcher fails" - refers to the OS browser
launcher (`open::that_detached`), not the TUI.
CLI behavior unchanged: bare `jackin`, `jackin console`, and deprecated
`jackin launch` all dispatch to the same code paths as before.
Addresses finding #3 of the second-pass PR #166 review.
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
…binaries Two test fixtures wrote shell scripts that used `echo -n VALUE` to emit a token without a trailing newline. POSIX `echo` does not specify `-n` behavior; on macOS `/bin/sh` (bash 3.2 in `sh` mode), `echo -n VALUE` outputs the literal `-n VALUE\n` instead of stripping the flag, which made these tests fail on macOS while passing on Linux/coreutils. Switch both fakes to `printf %s VALUE`, which is portable and emits exactly the bytes the tests assert on: - `src/operator_env.rs::op_cli_invokes_binary_and_returns_stdout` - `src/runtime/launch.rs::load_agent_injects_op_cli_resolved_value` The neighbouring `echo '2.30.0'` (--version reachability probe) is left as-is — it intentionally emits a trailing newline and the caller treats it as a yes/no exit-code probe rather than parsing the version string. Identified during second-pass PR #166 verification on macOS. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
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>
Implementation plans under `docs/superpowers/plans/` are local engineering artefacts, not part of the public docs surface. They should not land in the repo on a per-feature basis. Remove this branch's plan file and add `docs/superpowers/plans/` to `.gitignore` so future plan files generated by the writing-plans workflow don't accidentally get staged. Specs under `docs/superpowers/specs/` stay tracked — they're the durable design record. Plans are working notes that supersede each other and don't belong in the long-term history. Note: five plan files from prior PRs remain tracked from earlier commits on `main`. They're left as-is; this commit only changes forward behavior. Removing them is a separate cleanup conversation. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
… canonical sugar explanation Strengthen the "Operator console" naming rule in `docs/AGENTS.md`: - `jackin console` is the canonical name in every doc page, including every mention after the first. - The bare `jackin` shortcut is explained **exactly once**, in `commands/console.mdx`. Other pages reference that page rather than re-explaining the shortcut. - Forms like `(\`jackin\` or \`jackin console\`)` are also disallowed — they read as two separate commands and confuse readers. Apply the rule to existing prose: - `guides/workspaces.mdx` (×2): `(\`jackin\` or \`jackin console\`)` → `(\`jackin console\`)`. - `getting-started/quickstart.mdx`: example uses `jackin console`; the dedicated three-line shortcut explanation is replaced with a one-line link to `commands/console.mdx`. `commands/console.mdx`'s "Running `jackin` with no subcommand opens the same console …" stays — it's the one canonical home for the explanation. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Introduce a single source of truth for deprecated-but-still-supported items so we can periodically review what's safe to remove instead of rediscovering deprecations through grep or support tickets. Initial entries seeded with what's already deprecated in the tree: - `jackin launch` CLI verb (deprecated in PR #166; routes to console with a stderr warning). - `auth_forward = "copy"` config value (deprecated alongside the auth-sync default; auto-migrated to "sync" on the next config write). Each entry records: type (cli/api/config/behavior), deprecated since, replacement, removal trigger, and the source files that implement the deprecation — so a future removal commit can find every site to delete. Wire the file in: - AGENTS.md: add DEPRECATED.md to the "Shared conventions" list. - RULES.md: add a "Deprecations" section requiring that any new deprecation be recorded in DEPRECATED.md in the same commit, and that the entry be deleted in the removal commit. 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-…
This was referenced Apr 26, 2026
donbeave
added a commit
that referenced
this pull request
Apr 26, 2026
…MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description Co-authored-by: Claude <noreply@anthropic.com>
donbeave
added a commit
that referenced
this pull request
Apr 26, 2026
…MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description 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 26, 2026
…MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description 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 26, 2026
…MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description 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 26, 2026
…MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description 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 26, 2026
…183) * docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec Primary goal shift: codebase must be verifiable for AI-generated code. - §0: replace generic description with explicit verifiability rationale (module contracts, localised concerns, types/behaviour separation) - §4 intro: add "Why structure matters for AI-generated code" section with audit-units table mapping each post-split file to one reviewable question - §4 4a: expand config/types.rs from description to full execution spec — exact type list, post-split mod.rs content, zero-change submodule guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T which resolves through mod.rs re-exports unchanged), impl-extension pattern already in use documented Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 14 — editor method map, app helpers, //! queue - §4 4c: config/editor.rs split is now execution-ready — complete 6-file method-to-file table with private helper placement verified (validate_candidate→io.rs, table_path_mut→mod.rs pub(super), auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig) - §4 4e: app/mod.rs split complete — all private helpers mapped (parse_auth_forward_mode_from_cli→config_cmd.rs, workspace_env_scope→workspace_cmd.rs, print_env_table note, remove_data_dir_if_exists→dispatch.rs) - §10 step 5: add //! priority queue — 10 files with draft content, prioritised by cold-landing impact and AI audit risk; selector.rs and instance/mod.rs explicitly document the /→__ invariant Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed - §4 4d: correct operator_env dependency graph — layers.rs imports both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable resolve_operator_env wrapper at line 797); still a valid DAG - §4 4f: verify trust.rs split safety — FnOnce injection pattern means launch_pipeline.rs has zero dependency on trust.rs; import chain documented; trust bypass audit now requires reading only ~60L - §9 OQ1 closed: op_cache.rs read in full — 4-level structure, per-level invalidation, no TTL/expiry (expiry handled at OpCli subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence - Fix duplicate Rule 3 section introduced by previous edit; add docker.rs co-location note as third edge case (three edge cases, not two) - Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171 (was listed as 782L); propose 6-file tab-by-tab split with auditability note on the security-adjacent Secrets tab - Add §10 execution-order note: 4a and 4c are independent — editor.rs imports AppConfig via crate::config re-exports regardless of 4a order - Append iteration 16 log entry with confidence table and weakest sections Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections - Add instance/auth.rs to //! priority queue at #4: four security invariants (0o600 perms, symlink rejection, TOCTOU-safe writes, macOS Keychain) documented in draft //! content - Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types mixed with impl blocks; propose 5-file types/behavior split - Correct stale line counts: render/list.rs 1122→1989 (PR #171 added render_environments_subpanel); state.rs 865→992; priorities upgraded - Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332, mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944 - Renumber //! priority queue to 11 entries (was 10) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal - Close OQ2: agent_allow.rs read in full — 55L, correct //! doc, design sound; serves as model for //! priority queue pattern - Add render/list.rs as new Rule 5 violator: 668L production (PR #171 added render_environments_subpanel); propose 3-file split (mod.rs, details.rs, subpanels.rs); note import-path change for agents_block_agent_count - Update §1 module map: agent_allow.rs entry corrected with size/API Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal - Correct input/editor.rs: 2349L total (was 1304L), 1141L production (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn handle_editor_modal at line 618 was invisible to previous grep pattern; now the largest production file in the codebase; priority → Critical - Correct input/save.rs: 1472L total, 661L production (was 567L) - Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers), secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs - Update key insight paragraph naming input/editor.rs as largest production file Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict - Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority order; rename existing 4f (launch.rs) → 4g; add circular-import risk note for ManagerStage/EditorState split sequencing - Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid is a tightly-coupled rendering loop); section comments compensate for missing //! - Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs; within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections - Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file split (mod.rs + flow.rs + preview.rs); no cross-dependency between flow and preview groups; §10 4f-v updated from Optional to concrete plan - Fix //! queue preamble: "first 10 files" → "first 11 files" - Correct save.rs module map (1418→1472L, correct key exports) and hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save is the Phase 2 partner at ~149L) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis - Analyze input/list.rs: 214L production (tests at 215); has //! doc; two focused pub(super) fns; no split needed; Low priority; correct module map - Add mount_info.rs to hot-spot table: 277L production; Low priority; has //! doc; correct module map with 3 public enums + inspect fn - Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected - Expand audit units table from 8 to 13 entries: add state/types.rs, state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs, input/save/preview.rs — all targeting PR #171 AI-generated console code - Add PR #171 context note linking 5 new entries to AI-generated code concern - Correct input/mod.rs module map: 369L, add InputOutcome enum to exports - Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed - Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded 1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted - Correct render/mod.rs module map: 421L; FooterItem + palette constants + render_header + centered_rect_fixed added to key exports - Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum) vs "Secrets / Environments" (UI label); /stub qualifier already removed Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed - Add console/mod.rs to hot-spot table: 406L/307L production (Low); correct module map from ~200 → 406L; note missing //! doc with ConsoleStage design block comment worth promoting - Add op_picker/render.rs to hot-spot table: 865L/545L production (Medium); PR #171 AI-generated; 14 functions in two logical groups (entry/helpers vs level renderers); split into levels.rs proposed - Correct 3 stale ~200L estimates for console/mod.rs across roadmap Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction - Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High); PR #171 AI-generated; OpPickerState types+behavior split opportunity; has 7-line //! doc; module map split into two rows (mod.rs + render.rs) - Add op_picker/render.rs 2-file split proposal: render.rs (coordinator) + render_pane.rs (pane/level renderers); no cross-dependency confirmed - Correct operator_env.rs total: 1569→2130L (880L production); update 4 occurrences across hot-spot table, ASCII tree, §4 analysis Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections - Add op_picker/mod.rs formal 3-file split: loading.rs (async load family ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors) - Correct "24 files" → "28+" for 500L threshold count - Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis - §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs → render.rs + pane.rs); document impl-extension and import-path caveats - §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files have //! docs, no file exceeds ~350L production; classified as exemplar (not a split candidate); document git_prompt.rs coupling-density justification and input.rs as 28-file false positive (144L production) - §1 module map: expand single file_browser/ row to 5 individual rows with production LOC and dominant concern per file Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections - §4: Add "Alternative thesis: documentation-first verification" — challenges the two core assumptions behind file splitting (files-as-audit-unit and file-size-as-context-constraint); adds 7-criterion comparison table vs structure-first approach; introduces phased combined recommendation: Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs, zero structural change); Phase 2 = splits only for >600L production files (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K - Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and 8 locations respectively; verified by fresh find|xargs wc -l scan) - §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L), source_picker.rs (244L) — all PR #171 additions with //! docs Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC - §4 alternative thesis: correct ">600L production → 4 files" claim introduced in iteration 30; re-verified all 9 candidate files via #[cfg(test)] line position; threshold must be >800L to get exactly 4 files (9 exceed 600L); add verification table with test-start lines - Production LOC corrections (5+ locations each): launch.rs 1085→~1077, operator_env.rs 810→~880, app/mod.rs 928→~957, config/editor.rs 503→~584 - §2 OpPicker row: replace vague "no entry yet" with confirmed gap: PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10 named); omits op_picker/, agent_picker.rs, scope_picker.rs, source_picker.rs and pre-dates the manager/ sub-structure split Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template - §8.1: Add two-tier spec architecture table distinguishing feature specs (public Starlight MDX, user-facing) from behavioral specs (internal docs/internal/specs/, for AI code verification) — resolves contradiction between §4 (which said docs/internal/specs/) and §8.1 (which said "no longer needed; specs are public") - §8.1: Add concrete behavioral spec template for op_picker/ with state machine table and 3 INV invariant entries each with a grep-executable "Verify by:" command; template directly usable for the 3 Phase 1 specs - §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim - Confirmed render/editor.rs ~736L and render/list.rs ~668L production (no interspersed production code — all test blocks follow consecutively) Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 33 — executive summary, §0 correctness - §0: Add executive summary (~300 words) with core problem, 3-phase recommendation, key counter-argument, and navigation table pointing to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a readability roadmap with no entry-point orientation - §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs current verified size; stale reference was in the first section readers see) - §0 item 3: Add "(selective)" qualifier and explicit note that standard Rust co-locates struct+impl — impl-extension pattern is justified only for files >800L production, not as a universal rule Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track - §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //! doc, ~1077L production, critical path — all jackin load failures trace here); drop config/editor.rs from Phase 1 (its 963L test suite already serves as behavioral spec — tests are behavioral examples); reduce Phase 1 from 3 specs to 2 specs; add reasoning for the priority ordering - §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling setup) + Track B (Phase 1 behavioral spec authoring); Track B includes specific INV invariants to capture for runtime/launch.rs grounded in reading the actual function structure (step comment positions); adds sequencing rationale: spec must precede structural splits Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from iteration 34 (inferred from step comment positions) with 5 verified INVs citing exact line numbers: - INV-1: trust gate (line 594) precedes image build (line 736) - INV-2: container name claimed (line 754) between image build and network - INV-3: token verified (line 763) before network creation (line 827) - INV-4: render_exit called at lines 886 AND 890 (all exit paths) - INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup, crash→disarm (explains jackin hardline compatibility) Corrected wrong line number: claim_container_name call is at 754, not 918 (918 is the function definition). Each INV has a grep-executable Verify by. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with three concrete options: - Option A: CONTRIBUTING.md rule (necessary but insufficient) - Option B: ci.yml git-diff-scoped shell check (recommended) — only checks files added in the current PR so it doesn't require fixing existing stale entries before merging; greps for module directory name in prose - Option C: Structured TOML module registry (over-engineered for scale) Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts pattern already established in docs/scripts/ Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture Iteration 36: - §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with 3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry); recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded in existing check:repo-links.ts pattern from docs/scripts/ Iteration 37 (operator directive: greenfield Rust structure): - §4: Add "Greenfield architecture — ideal structure for a growing project" section based on verified cross-module dependency graph (grep iteration 37) - Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0; config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3 - Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config) - Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui, jackin-runtime, jackin-console, jackin-shell + thin binary - Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary - Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 38 — Rust workspace standards, community evidence Ground workspace recommendation in real-world project research: - ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers - starship and fd-find stay single-crate at 1M+ LOC — no library use case - jackin (43K LOC, no external consumers) maps to starship/fd pattern → single-crate is community-standard; "stay single-crate" recommendation confirmed Update greenfield workspace structure to follow matklad's pattern: - Virtual manifest at root (no [package] in root Cargo.toml) - Flat crates/ directory (not nested); crate names match folder names - version = "0.0.0" for unpublished internal crates - Add inline dep comments to each crate in the ASCII structure Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings + Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22) Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md §7.9: Reverse previous "reject" recommendation to "adopt" per-directory README.md for major src/ module directories. Rationale: README.md is AI-native — Claude Code, Copilot, Cursor load it automatically on directory entry, giving AI agents orientation before they decide which file to open. PROJECT_STRUCTURE.md being confirmed stale removes the main argument for the "single root file" approach. Add three-layer documentation model table: - README.md: directory orientation (AI + human, on entry) - AGENTS.md: agent workflow rules (root, session start) - CLAUDE.md: @AGENTS.md pointer only — NEVER add content here - //! docs: file-level contracts (when reading/editing) Add specific README.md content targets for 7 directories (src/, src/runtime/, src/console/, src/console/manager/, src/console/widgets/, docs/, docs/internal/). §3 target document shape: Add per-directory README.md to proposed hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md design principle (single-line @AGENTS.md — never duplicate content). Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): internal docs are browsable — unified Starlight site Operator directive: internal docs (architecture, specs, ADRs, roadmap) should be browsable, not hidden filesystem files. They are a different TYPE of docs focused on implementation details and vision, published as a "Developer Reference" section of the Starlight site. §3 target document shape: - docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages) - Browsable at jackin.tailrocks.com/internal/ - Sidebar: "Developer Reference" group (collapsed by default) with sub-sections for architecture, code-tour, contributing, testing, decisions, specs, roadmap - Include astro.config.ts sidebar config snippet §8.1 two-tier spec distinction eliminated: - Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/ - Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location - Both browsable and searchable via Starlight; AI agents can be pointed to URLs §8.3 + §4: - All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/ - ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable) - README.md pointer for src/runtime/ updated to URL reference Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): §11 — modern Rust docs platform (future project) Add §11 capturing the vision for a modern docs.rs alternative with: - rustdoc JSON ingestion → Astro Starlight presentation - MCP server for AI agent queries (Context7 alternative for Rust) - Rust-specific query types: rust_get_context(), rust_find_impls(), rust_search_types() — things Context7 cannot provide - Comparison table vs Context7 - Architecture diagram (ingestion → processing → Starlight + MCP) - Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs - Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional prototype for the platform's processing and presentation layers Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 39 — update §0, fix stale internal/ paths §0 executive summary: rewrite to reflect decisions from iterations 30-38: - browsable internal docs (jackin.tailrocks.com/internal/) - per-directory README.md adoption (§7.9 reversed) - CLAUDE.md = @AGENTS.md single-line pointer only - greenfield workspace architecture (matklad's virtual manifest pattern) - §11 future project: modern Rust docs platform / Context7-for-Rust - document size 1800+ → 2200+ Fix stale docs/internal/ bare paths not caught by iteration 38 sweep: - Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths - §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path - §10 Track B item 2: op-picker spec path → Starlight MDX Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit §7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline - Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended) / rustdoc-json crate as Rust binary - Option B recommended: matches existing docs/scripts/ pattern, nightly isolated to separate CI step, zero effect on stable build - Key design: URL at /internal/api/, cross-links to behavioral specs, Starlight unified search, prototype for §11 future project - Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit - Recommend: adopt after Phase 1 //! sprint (value ∝ coverage) §4 Rule 4 pub discipline: replace estimated "50-100 items" guess with verified numbers from iteration 40 grep: - 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files - 0 uses of unreachable_pub lint — no enforcement gate - Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8) - Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn" - Revised scope: ~150-200 mechanical conversions (excludes entry points) Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): split research into 19 actionable items Delete _research_notes.md (no longer needed). Replace 2343L READABILITY_AND_MODERNIZATION.md with: - README.md: index of all 19 items with phase, ordering notes, links - READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L) - items/ITEM-001 through ITEM-019: individual actionable items Items by phase: Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011 Phase 1 (needs confirmation): ITEM-005, 016, 018 Phase 2 (structural splits, confirmation required): ITEM-012..015 Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace) Each item has: summary, key files with line numbers, steps, what needs confirmation, and relevant research backing from the 40-iteration analysis loop. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): migrate 19 items to Starlight reference/roadmap section Move all codebase health roadmap items from docs/internal/roadmap/items/ (plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/ (MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/). Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to astro.config.ts. Deletes the old items/ directory. Updates the internal README to redirect to the new location. Also adds codebase-readability.mdx — a new overview item that captures the overall readability/restructuring program with a recommended execution order: file splits first, then greenfield workspace, then per-directory README+AGENTS.md, then docs and specs. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): remove premature internal/roadmap/README.md The internal/ structure doesn't exist yet — it will be created as part of the roadmap items themselves. No need for a redirect stub now. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive All content has been distilled into the individual Starlight roadmap pages. The full 2343L research is preserved in git history at commit b7e9fc2. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): fix check:repo-links errors + remove iteration log - Replace plain code spans with <RepoFile> for validate.rs, mise.toml, Cargo.toml, and op_picker/mod.rs - Remove deleted READABILITY_AND_MODERNIZATION.md reference from codebase-readability.mdx - Delete _iteration_log.md (git history is the archive) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): fix lychee false-positive link in move-contributing-testing The example redirect text contained a markdown hyperlink to a proposed future file path that doesn't exist yet. Changed to a code span. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.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 6, 2026
…183) * docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec Primary goal shift: codebase must be verifiable for AI-generated code. - §0: replace generic description with explicit verifiability rationale (module contracts, localised concerns, types/behaviour separation) - §4 intro: add "Why structure matters for AI-generated code" section with audit-units table mapping each post-split file to one reviewable question - §4 4a: expand config/types.rs from description to full execution spec — exact type list, post-split mod.rs content, zero-change submodule guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T which resolves through mod.rs re-exports unchanged), impl-extension pattern already in use documented Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 14 — editor method map, app helpers, //! queue - §4 4c: config/editor.rs split is now execution-ready — complete 6-file method-to-file table with private helper placement verified (validate_candidate→io.rs, table_path_mut→mod.rs pub(super), auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig) - §4 4e: app/mod.rs split complete — all private helpers mapped (parse_auth_forward_mode_from_cli→config_cmd.rs, workspace_env_scope→workspace_cmd.rs, print_env_table note, remove_data_dir_if_exists→dispatch.rs) - §10 step 5: add //! priority queue — 10 files with draft content, prioritised by cold-landing impact and AI audit risk; selector.rs and instance/mod.rs explicitly document the /→__ invariant Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed - §4 4d: correct operator_env dependency graph — layers.rs imports both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable resolve_operator_env wrapper at line 797); still a valid DAG - §4 4f: verify trust.rs split safety — FnOnce injection pattern means launch_pipeline.rs has zero dependency on trust.rs; import chain documented; trust bypass audit now requires reading only ~60L - §9 OQ1 closed: op_cache.rs read in full — 4-level structure, per-level invalidation, no TTL/expiry (expiry handled at OpCli subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence - Fix duplicate Rule 3 section introduced by previous edit; add docker.rs co-location note as third edge case (three edge cases, not two) - Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171 (was listed as 782L); propose 6-file tab-by-tab split with auditability note on the security-adjacent Secrets tab - Add §10 execution-order note: 4a and 4c are independent — editor.rs imports AppConfig via crate::config re-exports regardless of 4a order - Append iteration 16 log entry with confidence table and weakest sections Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections - Add instance/auth.rs to //! priority queue at #4: four security invariants (0o600 perms, symlink rejection, TOCTOU-safe writes, macOS Keychain) documented in draft //! content - Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types mixed with impl blocks; propose 5-file types/behavior split - Correct stale line counts: render/list.rs 1122→1989 (PR #171 added render_environments_subpanel); state.rs 865→992; priorities upgraded - Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332, mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944 - Renumber //! priority queue to 11 entries (was 10) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal - Close OQ2: agent_allow.rs read in full — 55L, correct //! doc, design sound; serves as model for //! priority queue pattern - Add render/list.rs as new Rule 5 violator: 668L production (PR #171 added render_environments_subpanel); propose 3-file split (mod.rs, details.rs, subpanels.rs); note import-path change for agents_block_agent_count - Update §1 module map: agent_allow.rs entry corrected with size/API Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal - Correct input/editor.rs: 2349L total (was 1304L), 1141L production (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn handle_editor_modal at line 618 was invisible to previous grep pattern; now the largest production file in the codebase; priority → Critical - Correct input/save.rs: 1472L total, 661L production (was 567L) - Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers), secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs - Update key insight paragraph naming input/editor.rs as largest production file Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict - Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority order; rename existing 4f (launch.rs) → 4g; add circular-import risk note for ManagerStage/EditorState split sequencing - Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid is a tightly-coupled rendering loop); section comments compensate for missing //! - Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs; within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections - Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file split (mod.rs + flow.rs + preview.rs); no cross-dependency between flow and preview groups; §10 4f-v updated from Optional to concrete plan - Fix //! queue preamble: "first 10 files" → "first 11 files" - Correct save.rs module map (1418→1472L, correct key exports) and hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save is the Phase 2 partner at ~149L) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis - Analyze input/list.rs: 214L production (tests at 215); has //! doc; two focused pub(super) fns; no split needed; Low priority; correct module map - Add mount_info.rs to hot-spot table: 277L production; Low priority; has //! doc; correct module map with 3 public enums + inspect fn - Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected - Expand audit units table from 8 to 13 entries: add state/types.rs, state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs, input/save/preview.rs — all targeting PR #171 AI-generated console code - Add PR #171 context note linking 5 new entries to AI-generated code concern - Correct input/mod.rs module map: 369L, add InputOutcome enum to exports - Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed - Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded 1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted - Correct render/mod.rs module map: 421L; FooterItem + palette constants + render_header + centered_rect_fixed added to key exports - Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum) vs "Secrets / Environments" (UI label); /stub qualifier already removed Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed - Add console/mod.rs to hot-spot table: 406L/307L production (Low); correct module map from ~200 → 406L; note missing //! doc with ConsoleStage design block comment worth promoting - Add op_picker/render.rs to hot-spot table: 865L/545L production (Medium); PR #171 AI-generated; 14 functions in two logical groups (entry/helpers vs level renderers); split into levels.rs proposed - Correct 3 stale ~200L estimates for console/mod.rs across roadmap Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction - Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High); PR #171 AI-generated; OpPickerState types+behavior split opportunity; has 7-line //! doc; module map split into two rows (mod.rs + render.rs) - Add op_picker/render.rs 2-file split proposal: render.rs (coordinator) + render_pane.rs (pane/level renderers); no cross-dependency confirmed - Correct operator_env.rs total: 1569→2130L (880L production); update 4 occurrences across hot-spot table, ASCII tree, §4 analysis Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections - Add op_picker/mod.rs formal 3-file split: loading.rs (async load family ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors) - Correct "24 files" → "28+" for 500L threshold count - Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis - §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs → render.rs + pane.rs); document impl-extension and import-path caveats - §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files have //! docs, no file exceeds ~350L production; classified as exemplar (not a split candidate); document git_prompt.rs coupling-density justification and input.rs as 28-file false positive (144L production) - §1 module map: expand single file_browser/ row to 5 individual rows with production LOC and dominant concern per file Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections - §4: Add "Alternative thesis: documentation-first verification" — challenges the two core assumptions behind file splitting (files-as-audit-unit and file-size-as-context-constraint); adds 7-criterion comparison table vs structure-first approach; introduces phased combined recommendation: Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs, zero structural change); Phase 2 = splits only for >600L production files (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K - Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and 8 locations respectively; verified by fresh find|xargs wc -l scan) - §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L), source_picker.rs (244L) — all PR #171 additions with //! docs Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC - §4 alternative thesis: correct ">600L production → 4 files" claim introduced in iteration 30; re-verified all 9 candidate files via #[cfg(test)] line position; threshold must be >800L to get exactly 4 files (9 exceed 600L); add verification table with test-start lines - Production LOC corrections (5+ locations each): launch.rs 1085→~1077, operator_env.rs 810→~880, app/mod.rs 928→~957, config/editor.rs 503→~584 - §2 OpPicker row: replace vague "no entry yet" with confirmed gap: PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10 named); omits op_picker/, agent_picker.rs, scope_picker.rs, source_picker.rs and pre-dates the manager/ sub-structure split Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template - §8.1: Add two-tier spec architecture table distinguishing feature specs (public Starlight MDX, user-facing) from behavioral specs (internal docs/internal/specs/, for AI code verification) — resolves contradiction between §4 (which said docs/internal/specs/) and §8.1 (which said "no longer needed; specs are public") - §8.1: Add concrete behavioral spec template for op_picker/ with state machine table and 3 INV invariant entries each with a grep-executable "Verify by:" command; template directly usable for the 3 Phase 1 specs - §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim - Confirmed render/editor.rs ~736L and render/list.rs ~668L production (no interspersed production code — all test blocks follow consecutively) Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 33 — executive summary, §0 correctness - §0: Add executive summary (~300 words) with core problem, 3-phase recommendation, key counter-argument, and navigation table pointing to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a readability roadmap with no entry-point orientation - §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs current verified size; stale reference was in the first section readers see) - §0 item 3: Add "(selective)" qualifier and explicit note that standard Rust co-locates struct+impl — impl-extension pattern is justified only for files >800L production, not as a universal rule Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track - §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //! doc, ~1077L production, critical path — all jackin load failures trace here); drop config/editor.rs from Phase 1 (its 963L test suite already serves as behavioral spec — tests are behavioral examples); reduce Phase 1 from 3 specs to 2 specs; add reasoning for the priority ordering - §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling setup) + Track B (Phase 1 behavioral spec authoring); Track B includes specific INV invariants to capture for runtime/launch.rs grounded in reading the actual function structure (step comment positions); adds sequencing rationale: spec must precede structural splits Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from iteration 34 (inferred from step comment positions) with 5 verified INVs citing exact line numbers: - INV-1: trust gate (line 594) precedes image build (line 736) - INV-2: container name claimed (line 754) between image build and network - INV-3: token verified (line 763) before network creation (line 827) - INV-4: render_exit called at lines 886 AND 890 (all exit paths) - INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup, crash→disarm (explains jackin hardline compatibility) Corrected wrong line number: claim_container_name call is at 754, not 918 (918 is the function definition). Each INV has a grep-executable Verify by. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with three concrete options: - Option A: CONTRIBUTING.md rule (necessary but insufficient) - Option B: ci.yml git-diff-scoped shell check (recommended) — only checks files added in the current PR so it doesn't require fixing existing stale entries before merging; greps for module directory name in prose - Option C: Structured TOML module registry (over-engineered for scale) Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts pattern already established in docs/scripts/ Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture Iteration 36: - §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with 3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry); recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded in existing check:repo-links.ts pattern from docs/scripts/ Iteration 37 (operator directive: greenfield Rust structure): - §4: Add "Greenfield architecture — ideal structure for a growing project" section based on verified cross-module dependency graph (grep iteration 37) - Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0; config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3 - Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config) - Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui, jackin-runtime, jackin-console, jackin-shell + thin binary - Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary - Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 38 — Rust workspace standards, community evidence Ground workspace recommendation in real-world project research: - ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers - starship and fd-find stay single-crate at 1M+ LOC — no library use case - jackin (43K LOC, no external consumers) maps to starship/fd pattern → single-crate is community-standard; "stay single-crate" recommendation confirmed Update greenfield workspace structure to follow matklad's pattern: - Virtual manifest at root (no [package] in root Cargo.toml) - Flat crates/ directory (not nested); crate names match folder names - version = "0.0.0" for unpublished internal crates - Add inline dep comments to each crate in the ASCII structure Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings + Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22) Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md §7.9: Reverse previous "reject" recommendation to "adopt" per-directory README.md for major src/ module directories. Rationale: README.md is AI-native — Claude Code, Copilot, Cursor load it automatically on directory entry, giving AI agents orientation before they decide which file to open. PROJECT_STRUCTURE.md being confirmed stale removes the main argument for the "single root file" approach. Add three-layer documentation model table: - README.md: directory orientation (AI + human, on entry) - AGENTS.md: agent workflow rules (root, session start) - CLAUDE.md: @AGENTS.md pointer only — NEVER add content here - //! docs: file-level contracts (when reading/editing) Add specific README.md content targets for 7 directories (src/, src/runtime/, src/console/, src/console/manager/, src/console/widgets/, docs/, docs/internal/). §3 target document shape: Add per-directory README.md to proposed hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md design principle (single-line @AGENTS.md — never duplicate content). Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): internal docs are browsable — unified Starlight site Operator directive: internal docs (architecture, specs, ADRs, roadmap) should be browsable, not hidden filesystem files. They are a different TYPE of docs focused on implementation details and vision, published as a "Developer Reference" section of the Starlight site. §3 target document shape: - docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages) - Browsable at jackin.tailrocks.com/internal/ - Sidebar: "Developer Reference" group (collapsed by default) with sub-sections for architecture, code-tour, contributing, testing, decisions, specs, roadmap - Include astro.config.ts sidebar config snippet §8.1 two-tier spec distinction eliminated: - Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/ - Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location - Both browsable and searchable via Starlight; AI agents can be pointed to URLs §8.3 + §4: - All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/ - ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable) - README.md pointer for src/runtime/ updated to URL reference Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): §11 — modern Rust docs platform (future project) Add §11 capturing the vision for a modern docs.rs alternative with: - rustdoc JSON ingestion → Astro Starlight presentation - MCP server for AI agent queries (Context7 alternative for Rust) - Rust-specific query types: rust_get_context(), rust_find_impls(), rust_search_types() — things Context7 cannot provide - Comparison table vs Context7 - Architecture diagram (ingestion → processing → Starlight + MCP) - Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs - Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional prototype for the platform's processing and presentation layers Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 39 — update §0, fix stale internal/ paths §0 executive summary: rewrite to reflect decisions from iterations 30-38: - browsable internal docs (jackin.tailrocks.com/internal/) - per-directory README.md adoption (§7.9 reversed) - CLAUDE.md = @AGENTS.md single-line pointer only - greenfield workspace architecture (matklad's virtual manifest pattern) - §11 future project: modern Rust docs platform / Context7-for-Rust - document size 1800+ → 2200+ Fix stale docs/internal/ bare paths not caught by iteration 38 sweep: - Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths - §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path - §10 Track B item 2: op-picker spec path → Starlight MDX Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit §7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline - Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended) / rustdoc-json crate as Rust binary - Option B recommended: matches existing docs/scripts/ pattern, nightly isolated to separate CI step, zero effect on stable build - Key design: URL at /internal/api/, cross-links to behavioral specs, Starlight unified search, prototype for §11 future project - Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit - Recommend: adopt after Phase 1 //! sprint (value ∝ coverage) §4 Rule 4 pub discipline: replace estimated "50-100 items" guess with verified numbers from iteration 40 grep: - 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files - 0 uses of unreachable_pub lint — no enforcement gate - Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8) - Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn" - Revised scope: ~150-200 mechanical conversions (excludes entry points) Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): split research into 19 actionable items Delete _research_notes.md (no longer needed). Replace 2343L READABILITY_AND_MODERNIZATION.md with: - README.md: index of all 19 items with phase, ordering notes, links - READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L) - items/ITEM-001 through ITEM-019: individual actionable items Items by phase: Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011 Phase 1 (needs confirmation): ITEM-005, 016, 018 Phase 2 (structural splits, confirmation required): ITEM-012..015 Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace) Each item has: summary, key files with line numbers, steps, what needs confirmation, and relevant research backing from the 40-iteration analysis loop. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): migrate 19 items to Starlight reference/roadmap section Move all codebase health roadmap items from docs/internal/roadmap/items/ (plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/ (MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/). Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to astro.config.ts. Deletes the old items/ directory. Updates the internal README to redirect to the new location. Also adds codebase-readability.mdx — a new overview item that captures the overall readability/restructuring program with a recommended execution order: file splits first, then greenfield workspace, then per-directory README+AGENTS.md, then docs and specs. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): remove premature internal/roadmap/README.md The internal/ structure doesn't exist yet — it will be created as part of the roadmap items themselves. No need for a redirect stub now. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive All content has been distilled into the individual Starlight roadmap pages. The full 2343L research is preserved in git history at commit b7e9fc2. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): fix check:repo-links errors + remove iteration log - Replace plain code spans with <RepoFile> for validate.rs, mise.toml, Cargo.toml, and op_picker/mod.rs - Remove deleted READABILITY_AND_MODERNIZATION.md reference from codebase-readability.mdx - Delete _iteration_log.md (git history is the archive) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> * docs(roadmap): fix lychee false-positive link in move-contributing-testing The example redirect text contained a markdown hyperlink to a proposed future file path that doesn't exist yet. Changed to a code span. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.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.
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
…183) * docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec Primary goal shift: codebase must be verifiable for AI-generated code. - §0: replace generic description with explicit verifiability rationale (module contracts, localised concerns, types/behaviour separation) - §4 intro: add "Why structure matters for AI-generated code" section with audit-units table mapping each post-split file to one reviewable question - §4 4a: expand config/types.rs from description to full execution spec — exact type list, post-split mod.rs content, zero-change submodule guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T which resolves through mod.rs re-exports unchanged), impl-extension pattern already in use documented Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 14 — editor method map, app helpers, //! queue - §4 4c: config/editor.rs split is now execution-ready — complete 6-file method-to-file table with private helper placement verified (validate_candidate→io.rs, table_path_mut→mod.rs pub(super), auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig) - §4 4e: app/mod.rs split complete — all private helpers mapped (parse_auth_forward_mode_from_cli→config_cmd.rs, workspace_env_scope→workspace_cmd.rs, print_env_table note, remove_data_dir_if_exists→dispatch.rs) - §10 step 5: add //! priority queue — 10 files with draft content, prioritised by cold-landing impact and AI audit risk; selector.rs and instance/mod.rs explicitly document the /→__ invariant Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed - §4 4d: correct operator_env dependency graph — layers.rs imports both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable resolve_operator_env wrapper at line 797); still a valid DAG - §4 4f: verify trust.rs split safety — FnOnce injection pattern means launch_pipeline.rs has zero dependency on trust.rs; import chain documented; trust bypass audit now requires reading only ~60L - §9 OQ1 closed: op_cache.rs read in full — 4-level structure, per-level invalidation, no TTL/expiry (expiry handled at OpCli subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence - Fix duplicate Rule 3 section introduced by previous edit; add docker.rs co-location note as third edge case (three edge cases, not two) - Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171 (was listed as 782L); propose 6-file tab-by-tab split with auditability note on the security-adjacent Secrets tab - Add §10 execution-order note: 4a and 4c are independent — editor.rs imports AppConfig via crate::config re-exports regardless of 4a order - Append iteration 16 log entry with confidence table and weakest sections Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections - Add instance/auth.rs to //! priority queue at #4: four security invariants (0o600 perms, symlink rejection, TOCTOU-safe writes, macOS Keychain) documented in draft //! content - Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types mixed with impl blocks; propose 5-file types/behavior split - Correct stale line counts: render/list.rs 1122→1989 (PR #171 added render_environments_subpanel); state.rs 865→992; priorities upgraded - Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332, mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944 - Renumber //! priority queue to 11 entries (was 10) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal - Close OQ2: agent_allow.rs read in full — 55L, correct //! doc, design sound; serves as model for //! priority queue pattern - Add render/list.rs as new Rule 5 violator: 668L production (PR #171 added render_environments_subpanel); propose 3-file split (mod.rs, details.rs, subpanels.rs); note import-path change for agents_block_agent_count - Update §1 module map: agent_allow.rs entry corrected with size/API Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal - Correct input/editor.rs: 2349L total (was 1304L), 1141L production (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn handle_editor_modal at line 618 was invisible to previous grep pattern; now the largest production file in the codebase; priority → Critical - Correct input/save.rs: 1472L total, 661L production (was 567L) - Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers), secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs - Update key insight paragraph naming input/editor.rs as largest production file Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict - Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority order; rename existing 4f (launch.rs) → 4g; add circular-import risk note for ManagerStage/EditorState split sequencing - Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid is a tightly-coupled rendering loop); section comments compensate for missing //! - Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs; within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections - Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file split (mod.rs + flow.rs + preview.rs); no cross-dependency between flow and preview groups; §10 4f-v updated from Optional to concrete plan - Fix //! queue preamble: "first 10 files" → "first 11 files" - Correct save.rs module map (1418→1472L, correct key exports) and hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save is the Phase 2 partner at ~149L) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis - Analyze input/list.rs: 214L production (tests at 215); has //! doc; two focused pub(super) fns; no split needed; Low priority; correct module map - Add mount_info.rs to hot-spot table: 277L production; Low priority; has //! doc; correct module map with 3 public enums + inspect fn - Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected - Expand audit units table from 8 to 13 entries: add state/types.rs, state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs, input/save/preview.rs — all targeting PR #171 AI-generated console code - Add PR #171 context note linking 5 new entries to AI-generated code concern - Correct input/mod.rs module map: 369L, add InputOutcome enum to exports - Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed - Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded 1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted - Correct render/mod.rs module map: 421L; FooterItem + palette constants + render_header + centered_rect_fixed added to key exports - Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum) vs "Secrets / Environments" (UI label); /stub qualifier already removed Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed - Add console/mod.rs to hot-spot table: 406L/307L production (Low); correct module map from ~200 → 406L; note missing //! doc with ConsoleStage design block comment worth promoting - Add op_picker/render.rs to hot-spot table: 865L/545L production (Medium); PR #171 AI-generated; 14 functions in two logical groups (entry/helpers vs level renderers); split into levels.rs proposed - Correct 3 stale ~200L estimates for console/mod.rs across roadmap Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction - Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High); PR #171 AI-generated; OpPickerState types+behavior split opportunity; has 7-line //! doc; module map split into two rows (mod.rs + render.rs) - Add op_picker/render.rs 2-file split proposal: render.rs (coordinator) + render_pane.rs (pane/level renderers); no cross-dependency confirmed - Correct operator_env.rs total: 1569→2130L (880L production); update 4 occurrences across hot-spot table, ASCII tree, §4 analysis Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections - Add op_picker/mod.rs formal 3-file split: loading.rs (async load family ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors) - Correct "24 files" → "28+" for 500L threshold count - Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis - §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs → render.rs + pane.rs); document impl-extension and import-path caveats - §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files have //! docs, no file exceeds ~350L production; classified as exemplar (not a split candidate); document git_prompt.rs coupling-density justification and input.rs as 28-file false positive (144L production) - §1 module map: expand single file_browser/ row to 5 individual rows with production LOC and dominant concern per file Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections - §4: Add "Alternative thesis: documentation-first verification" — challenges the two core assumptions behind file splitting (files-as-audit-unit and file-size-as-context-constraint); adds 7-criterion comparison table vs structure-first approach; introduces phased combined recommendation: Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs, zero structural change); Phase 2 = splits only for >600L production files (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K - Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and 8 locations respectively; verified by fresh find|xargs wc -l scan) - §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L), source_picker.rs (244L) — all PR #171 additions with //! docs Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC - §4 alternative thesis: correct ">600L production → 4 files" claim introduced in iteration 30; re-verified all 9 candidate files via #[cfg(test)] line position; threshold must be >800L to get exactly 4 files (9 exceed 600L); add verification table with test-start lines - Production LOC corrections (5+ locations each): launch.rs 1085→~1077, operator_env.rs 810→~880, app/mod.rs 928→~957, config/editor.rs 503→~584 - §2 OpPicker row: replace vague "no entry yet" with confirmed gap: PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10 named); omits op_picker/, agent_picker.rs, scope_picker.rs, source_picker.rs and pre-dates the manager/ sub-structure split Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template - §8.1: Add two-tier spec architecture table distinguishing feature specs (public Starlight MDX, user-facing) from behavioral specs (internal docs/internal/specs/, for AI code verification) — resolves contradiction between §4 (which said docs/internal/specs/) and §8.1 (which said "no longer needed; specs are public") - §8.1: Add concrete behavioral spec template for op_picker/ with state machine table and 3 INV invariant entries each with a grep-executable "Verify by:" command; template directly usable for the 3 Phase 1 specs - §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim - Confirmed render/editor.rs ~736L and render/list.rs ~668L production (no interspersed production code — all test blocks follow consecutively) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 33 — executive summary, §0 correctness - §0: Add executive summary (~300 words) with core problem, 3-phase recommendation, key counter-argument, and navigation table pointing to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a readability roadmap with no entry-point orientation - §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs current verified size; stale reference was in the first section readers see) - §0 item 3: Add "(selective)" qualifier and explicit note that standard Rust co-locates struct+impl — impl-extension pattern is justified only for files >800L production, not as a universal rule Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track - §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //! doc, ~1077L production, critical path — all jackin load failures trace here); drop config/editor.rs from Phase 1 (its 963L test suite already serves as behavioral spec — tests are behavioral examples); reduce Phase 1 from 3 specs to 2 specs; add reasoning for the priority ordering - §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling setup) + Track B (Phase 1 behavioral spec authoring); Track B includes specific INV invariants to capture for runtime/launch.rs grounded in reading the actual function structure (step comment positions); adds sequencing rationale: spec must precede structural splits Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from iteration 34 (inferred from step comment positions) with 5 verified INVs citing exact line numbers: - INV-1: trust gate (line 594) precedes image build (line 736) - INV-2: container name claimed (line 754) between image build and network - INV-3: token verified (line 763) before network creation (line 827) - INV-4: render_exit called at lines 886 AND 890 (all exit paths) - INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup, crash→disarm (explains jackin hardline compatibility) Corrected wrong line number: claim_container_name call is at 754, not 918 (918 is the function definition). Each INV has a grep-executable Verify by. Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with three concrete options: - Option A: CONTRIBUTING.md rule (necessary but insufficient) - Option B: ci.yml git-diff-scoped shell check (recommended) — only checks files added in the current PR so it doesn't require fixing existing stale entries before merging; greps for module directory name in prose - Option C: Structured TOML module registry (over-engineered for scale) Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts pattern already established in docs/scripts/ Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture Iteration 36: - §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with 3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry); recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded in existing check:repo-links.ts pattern from docs/scripts/ Iteration 37 (operator directive: greenfield Rust structure): - §4: Add "Greenfield architecture — ideal structure for a growing project" section based on verified cross-module dependency graph (grep iteration 37) - Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0; config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3 - Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config) - Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui, jackin-runtime, jackin-console, jackin-shell + thin binary - Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary - Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 38 — Rust workspace standards, community evidence Ground workspace recommendation in real-world project research: - ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers - starship and fd-find stay single-crate at 1M+ LOC — no library use case - jackin (43K LOC, no external consumers) maps to starship/fd pattern → single-crate is community-standard; "stay single-crate" recommendation confirmed Update greenfield workspace structure to follow matklad's pattern: - Virtual manifest at root (no [package] in root Cargo.toml) - Flat crates/ directory (not nested); crate names match folder names - version = "0.0.0" for unpublished internal crates - Add inline dep comments to each crate in the ASCII structure Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings + Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md §7.9: Reverse previous "reject" recommendation to "adopt" per-directory README.md for major src/ module directories. Rationale: README.md is AI-native — Claude Code, Copilot, Cursor load it automatically on directory entry, giving AI agents orientation before they decide which file to open. PROJECT_STRUCTURE.md being confirmed stale removes the main argument for the "single root file" approach. Add three-layer documentation model table: - README.md: directory orientation (AI + human, on entry) - AGENTS.md: agent workflow rules (root, session start) - CLAUDE.md: @AGENTS.md pointer only — NEVER add content here - //! docs: file-level contracts (when reading/editing) Add specific README.md content targets for 7 directories (src/, src/runtime/, src/console/, src/console/manager/, src/console/widgets/, docs/, docs/internal/). §3 target document shape: Add per-directory README.md to proposed hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md design principle (single-line @AGENTS.md — never duplicate content). Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): internal docs are browsable — unified Starlight site Operator directive: internal docs (architecture, specs, ADRs, roadmap) should be browsable, not hidden filesystem files. They are a different TYPE of docs focused on implementation details and vision, published as a "Developer Reference" section of the Starlight site. §3 target document shape: - docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages) - Browsable at jackin.tailrocks.com/internal/ - Sidebar: "Developer Reference" group (collapsed by default) with sub-sections for architecture, code-tour, contributing, testing, decisions, specs, roadmap - Include astro.config.ts sidebar config snippet §8.1 two-tier spec distinction eliminated: - Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/ - Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location - Both browsable and searchable via Starlight; AI agents can be pointed to URLs §8.3 + §4: - All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/ - ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable) - README.md pointer for src/runtime/ updated to URL reference Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): §11 — modern Rust docs platform (future project) Add §11 capturing the vision for a modern docs.rs alternative with: - rustdoc JSON ingestion → Astro Starlight presentation - MCP server for AI agent queries (Context7 alternative for Rust) - Rust-specific query types: rust_get_context(), rust_find_impls(), rust_search_types() — things Context7 cannot provide - Comparison table vs Context7 - Architecture diagram (ingestion → processing → Starlight + MCP) - Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs - Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional prototype for the platform's processing and presentation layers Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 39 — update §0, fix stale internal/ paths §0 executive summary: rewrite to reflect decisions from iterations 30-38: - browsable internal docs (jackin.tailrocks.com/internal/) - per-directory README.md adoption (§7.9 reversed) - CLAUDE.md = @AGENTS.md single-line pointer only - greenfield workspace architecture (matklad's virtual manifest pattern) - §11 future project: modern Rust docs platform / Context7-for-Rust - document size 1800+ → 2200+ Fix stale docs/internal/ bare paths not caught by iteration 38 sweep: - Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths - §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path - §10 Track B item 2: op-picker spec path → Starlight MDX Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit §7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline - Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended) / rustdoc-json crate as Rust binary - Option B recommended: matches existing docs/scripts/ pattern, nightly isolated to separate CI step, zero effect on stable build - Key design: URL at /internal/api/, cross-links to behavioral specs, Starlight unified search, prototype for §11 future project - Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit - Recommend: adopt after Phase 1 //! sprint (value ∝ coverage) §4 Rule 4 pub discipline: replace estimated "50-100 items" guess with verified numbers from iteration 40 grep: - 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files - 0 uses of unreachable_pub lint — no enforcement gate - Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8) - Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn" - Revised scope: ~150-200 mechanical conversions (excludes entry points) Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): split research into 19 actionable items Delete _research_notes.md (no longer needed). Replace 2343L READABILITY_AND_MODERNIZATION.md with: - README.md: index of all 19 items with phase, ordering notes, links - READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L) - items/ITEM-001 through ITEM-019: individual actionable items Items by phase: Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011 Phase 1 (needs confirmation): ITEM-005, 016, 018 Phase 2 (structural splits, confirmation required): ITEM-012..015 Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace) Each item has: summary, key files with line numbers, steps, what needs confirmation, and relevant research backing from the 40-iteration analysis loop. Co-authored-by: Claude <noreply@anthropic.com> * docs(roadmap): migrate 19 items to Starlight reference/roadmap section Move all codebase health roadmap items from docs/internal/roadmap/items/ (plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/ (MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/). Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to astro.config.ts. Deletes the old items/ directory. Updates the internal README to redirect to the new location. Also adds codebase-readability.mdx — a new overview item that captures the overall readability/restructuring program with a recommended execution order: file splits first, then greenfield workspace, then per-directory README+AGENTS.md, then docs and specs. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> * docs(roadmap): remove premature internal/roadmap/README.md The internal/ structure doesn't exist yet — it will be created as part of the roadmap items themselves. No need for a redirect stub now. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> * docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive All content has been distilled into the individual Starlight roadmap pages. The full 2343L research is preserved in git history at commit b7e9fc2. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> * docs(roadmap): fix check:repo-links errors + remove iteration log - Replace plain code spans with <RepoFile> for validate.rs, mise.toml, Cargo.toml, and op_picker/mod.rs - Remove deleted READABILITY_AND_MODERNIZATION.md reference from codebase-readability.mdx - Delete _iteration_log.md (git history is the archive) Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> * docs(roadmap): fix lychee false-positive link in move-contributing-testing The example redirect text contained a markdown hyperlink to a proposed future file path that doesn't exist yet. Changed to a code span. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Alexey Zhokhov <alexey@chainargos.com> --------- Signed-off-by: Alexey Zhokhov <alexey@chainargos.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.
* 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
…183) * docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec Primary goal shift: codebase must be verifiable for AI-generated code. - §0: replace generic description with explicit verifiability rationale (module contracts, localised concerns, types/behaviour separation) - §4 intro: add "Why structure matters for AI-generated code" section with audit-units table mapping each post-split file to one reviewable question - §4 4a: expand config/types.rs from description to full execution spec — exact type list, post-split mod.rs content, zero-change submodule guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T which resolves through mod.rs re-exports unchanged), impl-extension pattern already in use documented * docs(roadmap): iteration 14 — editor method map, app helpers, //! queue - §4 4c: config/editor.rs split is now execution-ready — complete 6-file method-to-file table with private helper placement verified (validate_candidate→io.rs, table_path_mut→mod.rs pub(super), auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig) - §4 4e: app/mod.rs split complete — all private helpers mapped (parse_auth_forward_mode_from_cli→config_cmd.rs, workspace_env_scope→workspace_cmd.rs, print_env_table note, remove_data_dir_if_exists→dispatch.rs) - §10 step 5: add //! priority queue — 10 files with draft content, prioritised by cold-landing impact and AI audit risk; selector.rs and instance/mod.rs explicitly document the /→__ invariant * docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed - §4 4d: correct operator_env dependency graph — layers.rs imports both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable resolve_operator_env wrapper at line 797); still a valid DAG - §4 4f: verify trust.rs split safety — FnOnce injection pattern means launch_pipeline.rs has zero dependency on trust.rs; import chain documented; trust bypass audit now requires reading only ~60L - §9 OQ1 closed: op_cache.rs read in full — 4-level structure, per-level invalidation, no TTL/expiry (expiry handled at OpCli subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented * docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence - Fix duplicate Rule 3 section introduced by previous edit; add docker.rs co-location note as third edge case (three edge cases, not two) - Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171 (was listed as 782L); propose 6-file tab-by-tab split with auditability note on the security-adjacent Secrets tab - Add §10 execution-order note: 4a and 4c are independent — editor.rs imports AppConfig via crate::config re-exports regardless of 4a order - Append iteration 16 log entry with confidence table and weakest sections * docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections - Add instance/auth.rs to //! priority queue at #4: four security invariants (0o600 perms, symlink rejection, TOCTOU-safe writes, macOS Keychain) documented in draft //! content - Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types mixed with impl blocks; propose 5-file types/behavior split - Correct stale line counts: render/list.rs 1122→1989 (PR #171 added render_environments_subpanel); state.rs 865→992; priorities upgraded - Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332, mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944 - Renumber //! priority queue to 11 entries (was 10) * docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal - Close OQ2: agent_allow.rs read in full — 55L, correct //! doc, design sound; serves as model for //! priority queue pattern - Add render/list.rs as new Rule 5 violator: 668L production (PR #171 added render_environments_subpanel); propose 3-file split (mod.rs, details.rs, subpanels.rs); note import-path change for agents_block_agent_count - Update §1 module map: agent_allow.rs entry corrected with size/API * docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal - Correct input/editor.rs: 2349L total (was 1304L), 1141L production (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn handle_editor_modal at line 618 was invisible to previous grep pattern; now the largest production file in the codebase; priority → Critical - Correct input/save.rs: 1472L total, 661L production (was 567L) - Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers), secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs - Update key insight paragraph naming input/editor.rs as largest production file * docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict - Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority order; rename existing 4f (launch.rs) → 4g; add circular-import risk note for ManagerStage/EditorState split sequencing - Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid is a tightly-coupled rendering loop); section comments compensate for missing //! - Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs; within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable) * docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections - Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file split (mod.rs + flow.rs + preview.rs); no cross-dependency between flow and preview groups; §10 4f-v updated from Optional to concrete plan - Fix //! queue preamble: "first 10 files" → "first 11 files" - Correct save.rs module map (1418→1472L, correct key exports) and hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save is the Phase 2 partner at ~149L) * docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis - Analyze input/list.rs: 214L production (tests at 215); has //! doc; two focused pub(super) fns; no split needed; Low priority; correct module map - Add mount_info.rs to hot-spot table: 277L production; Low priority; has //! doc; correct module map with 3 public enums + inspect fn - Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists * docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected - Expand audit units table from 8 to 13 entries: add state/types.rs, state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs, input/save/preview.rs — all targeting PR #171 AI-generated console code - Add PR #171 context note linking 5 new entries to AI-generated code concern - Correct input/mod.rs module map: 369L, add InputOutcome enum to exports - Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct * docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed - Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded 1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted - Correct render/mod.rs module map: 421L; FooterItem + palette constants + render_header + centered_rect_fixed added to key exports - Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum) vs "Secrets / Environments" (UI label); /stub qualifier already removed * docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description * docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed - Add console/mod.rs to hot-spot table: 406L/307L production (Low); correct module map from ~200 → 406L; note missing //! doc with ConsoleStage design block comment worth promoting - Add op_picker/render.rs to hot-spot table: 865L/545L production (Medium); PR #171 AI-generated; 14 functions in two logical groups (entry/helpers vs level renderers); split into levels.rs proposed - Correct 3 stale ~200L estimates for console/mod.rs across roadmap * docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction - Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High); PR #171 AI-generated; OpPickerState types+behavior split opportunity; has 7-line //! doc; module map split into two rows (mod.rs + render.rs) - Add op_picker/render.rs 2-file split proposal: render.rs (coordinator) + render_pane.rs (pane/level renderers); no cross-dependency confirmed - Correct operator_env.rs total: 1569→2130L (880L production); update 4 occurrences across hot-spot table, ASCII tree, §4 analysis * docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections - Add op_picker/mod.rs formal 3-file split: loading.rs (async load family ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors) - Correct "24 files" → "28+" for 500L threshold count - Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note) * docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis - §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs → render.rs + pane.rs); document impl-extension and import-path caveats - §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files have //! docs, no file exceeds ~350L production; classified as exemplar (not a split candidate); document git_prompt.rs coupling-density justification and input.rs as 28-file false positive (144L production) - §1 module map: expand single file_browser/ row to 5 individual rows with production LOC and dominant concern per file * docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections - §4: Add "Alternative thesis: documentation-first verification" — challenges the two core assumptions behind file splitting (files-as-audit-unit and file-size-as-context-constraint); adds 7-criterion comparison table vs structure-first approach; introduces phased combined recommendation: Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs, zero structural change); Phase 2 = splits only for >600L production files (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K - Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and 8 locations respectively; verified by fresh find|xargs wc -l scan) - §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L), source_picker.rs (244L) — all PR #171 additions with //! docs * docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC - §4 alternative thesis: correct ">600L production → 4 files" claim introduced in iteration 30; re-verified all 9 candidate files via #[cfg(test)] line position; threshold must be >800L to get exactly 4 files (9 exceed 600L); add verification table with test-start lines - Production LOC corrections (5+ locations each): launch.rs 1085→~1077, operator_env.rs 810→~880, app/mod.rs 928→~957, config/editor.rs 503→~584 - §2 OpPicker row: replace vague "no entry yet" with confirmed gap: PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10 named); omits op_picker/, agent_picker.rs, scope_picker.rs, source_picker.rs and pre-dates the manager/ sub-structure split * docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template - §8.1: Add two-tier spec architecture table distinguishing feature specs (public Starlight MDX, user-facing) from behavioral specs (internal docs/internal/specs/, for AI code verification) — resolves contradiction between §4 (which said docs/internal/specs/) and §8.1 (which said "no longer needed; specs are public") - §8.1: Add concrete behavioral spec template for op_picker/ with state machine table and 3 INV invariant entries each with a grep-executable "Verify by:" command; template directly usable for the 3 Phase 1 specs - §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim - Confirmed render/editor.rs ~736L and render/list.rs ~668L production (no interspersed production code — all test blocks follow consecutively) * docs(roadmap): iteration 33 — executive summary, §0 correctness - §0: Add executive summary (~300 words) with core problem, 3-phase recommendation, key counter-argument, and navigation table pointing to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a readability roadmap with no entry-point orientation - §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs current verified size; stale reference was in the first section readers see) - §0 item 3: Add "(selective)" qualifier and explicit note that standard Rust co-locates struct+impl — impl-extension pattern is justified only for files >800L production, not as a universal rule * docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track - §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //! doc, ~1077L production, critical path — all jackin load failures trace here); drop config/editor.rs from Phase 1 (its 963L test suite already serves as behavioral spec — tests are behavioral examples); reduce Phase 1 from 3 specs to 2 specs; add reasoning for the priority ordering - §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling setup) + Track B (Phase 1 behavioral spec authoring); Track B includes specific INV invariants to capture for runtime/launch.rs grounded in reading the actual function structure (step comment positions); adds sequencing rationale: spec must precede structural splits * docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from iteration 34 (inferred from step comment positions) with 5 verified INVs citing exact line numbers: - INV-1: trust gate (line 594) precedes image build (line 736) - INV-2: container name claimed (line 754) between image build and network - INV-3: token verified (line 763) before network creation (line 827) - INV-4: render_exit called at lines 886 AND 890 (all exit paths) - INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup, crash→disarm (explains jackin hardline compatibility) Corrected wrong line number: claim_container_name call is at 754, not 918 (918 is the function definition). Each INV has a grep-executable Verify by. * docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with three concrete options: - Option A: CONTRIBUTING.md rule (necessary but insufficient) - Option B: ci.yml git-diff-scoped shell check (recommended) — only checks files added in the current PR so it doesn't require fixing existing stale entries before merging; greps for module directory name in prose - Option C: Structured TOML module registry (over-engineered for scale) Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts pattern already established in docs/scripts/ * docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture Iteration 36: - §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with 3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry); recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded in existing check:repo-links.ts pattern from docs/scripts/ Iteration 37 (operator directive: greenfield Rust structure): - §4: Add "Greenfield architecture — ideal structure for a growing project" section based on verified cross-module dependency graph (grep iteration 37) - Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0; config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3 - Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config) - Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui, jackin-runtime, jackin-console, jackin-shell + thin binary - Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary - Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration * docs(roadmap): iteration 38 — Rust workspace standards, community evidence Ground workspace recommendation in real-world project research: - ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers - starship and fd-find stay single-crate at 1M+ LOC — no library use case - jackin (43K LOC, no external consumers) maps to starship/fd pattern → single-crate is community-standard; "stay single-crate" recommendation confirmed Update greenfield workspace structure to follow matklad's pattern: - Virtual manifest at root (no [package] in root Cargo.toml) - Flat crates/ directory (not nested); crate names match folder names - version = "0.0.0" for unpublished internal crates - Add inline dep comments to each crate in the ASCII structure Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings + Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22) * docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md §7.9: Reverse previous "reject" recommendation to "adopt" per-directory README.md for major src/ module directories. Rationale: README.md is AI-native — Claude Code, Copilot, Cursor load it automatically on directory entry, giving AI agents orientation before they decide which file to open. PROJECT_STRUCTURE.md being confirmed stale removes the main argument for the "single root file" approach. Add three-layer documentation model table: - README.md: directory orientation (AI + human, on entry) - AGENTS.md: agent workflow rules (root, session start) - CLAUDE.md: @AGENTS.md pointer only — NEVER add content here - //! docs: file-level contracts (when reading/editing) Add specific README.md content targets for 7 directories (src/, src/runtime/, src/console/, src/console/manager/, src/console/widgets/, docs/, docs/internal/). §3 target document shape: Add per-directory README.md to proposed hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md design principle (single-line @AGENTS.md — never duplicate content). * docs(roadmap): internal docs are browsable — unified Starlight site Operator directive: internal docs (architecture, specs, ADRs, roadmap) should be browsable, not hidden filesystem files. They are a different TYPE of docs focused on implementation details and vision, published as a "Developer Reference" section of the Starlight site. §3 target document shape: - docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages) - Browsable at jackin.tailrocks.com/internal/ - Sidebar: "Developer Reference" group (collapsed by default) with sub-sections for architecture, code-tour, contributing, testing, decisions, specs, roadmap - Include astro.config.ts sidebar config snippet §8.1 two-tier spec distinction eliminated: - Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/ - Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location - Both browsable and searchable via Starlight; AI agents can be pointed to URLs §8.3 + §4: - All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/ - ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable) - README.md pointer for src/runtime/ updated to URL reference * docs(roadmap): §11 — modern Rust docs platform (future project) Add §11 capturing the vision for a modern docs.rs alternative with: - rustdoc JSON ingestion → Astro Starlight presentation - MCP server for AI agent queries (Context7 alternative for Rust) - Rust-specific query types: rust_get_context(), rust_find_impls(), rust_search_types() — things Context7 cannot provide - Comparison table vs Context7 - Architecture diagram (ingestion → processing → Starlight + MCP) - Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs - Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional prototype for the platform's processing and presentation layers * docs(roadmap): iteration 39 — update §0, fix stale internal/ paths §0 executive summary: rewrite to reflect decisions from iterations 30-38: - browsable internal docs (jackin.tailrocks.com/internal/) - per-directory README.md adoption (§7.9 reversed) - CLAUDE.md = @AGENTS.md single-line pointer only - greenfield workspace architecture (matklad's virtual manifest pattern) - §11 future project: modern Rust docs platform / Context7-for-Rust - document size 1800+ → 2200+ Fix stale docs/internal/ bare paths not caught by iteration 38 sweep: - Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths - §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path - §10 Track B item 2: op-picker spec path → Starlight MDX * docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit §7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline - Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended) / rustdoc-json crate as Rust binary - Option B recommended: matches existing docs/scripts/ pattern, nightly isolated to separate CI step, zero effect on stable build - Key design: URL at /internal/api/, cross-links to behavioral specs, Starlight unified search, prototype for §11 future project - Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit - Recommend: adopt after Phase 1 //! sprint (value ∝ coverage) §4 Rule 4 pub discipline: replace estimated "50-100 items" guess with verified numbers from iteration 40 grep: - 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files - 0 uses of unreachable_pub lint — no enforcement gate - Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8) - Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn" - Revised scope: ~150-200 mechanical conversions (excludes entry points) * docs(roadmap): split research into 19 actionable items Delete _research_notes.md (no longer needed). Replace 2343L READABILITY_AND_MODERNIZATION.md with: - README.md: index of all 19 items with phase, ordering notes, links - READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L) - items/ITEM-001 through ITEM-019: individual actionable items Items by phase: Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011 Phase 1 (needs confirmation): ITEM-005, 016, 018 Phase 2 (structural splits, confirmation required): ITEM-012..015 Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace) Each item has: summary, key files with line numbers, steps, what needs confirmation, and relevant research backing from the 40-iteration analysis loop. * docs(roadmap): migrate 19 items to Starlight reference/roadmap section Move all codebase health roadmap items from docs/internal/roadmap/items/ (plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/ (MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/). Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to astro.config.ts. Deletes the old items/ directory. Updates the internal README to redirect to the new location. Also adds codebase-readability.mdx — a new overview item that captures the overall readability/restructuring program with a recommended execution order: file splits first, then greenfield workspace, then per-directory README+AGENTS.md, then docs and specs. * docs(roadmap): remove premature internal/roadmap/README.md The internal/ structure doesn't exist yet — it will be created as part of the roadmap items themselves. No need for a redirect stub now. * docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive All content has been distilled into the individual Starlight roadmap pages. The full 2343L research is preserved in git history at commit b7e9fc2. * docs(roadmap): fix check:repo-links errors + remove iteration log - Replace plain code spans with <RepoFile> for validate.rs, mise.toml, Cargo.toml, and op_picker/mod.rs - Remove deleted READABILITY_AND_MODERNIZATION.md reference from codebase-readability.mdx - Delete _iteration_log.md (git history is the archive) * docs(roadmap): fix lychee false-positive link in move-contributing-testing The example redirect text contained a markdown hyperlink to a proposed future file path that doesn't exist yet. Changed to a code span. --------- 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
…183) * docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec Primary goal shift: codebase must be verifiable for AI-generated code. - §0: replace generic description with explicit verifiability rationale (module contracts, localised concerns, types/behaviour separation) - §4 intro: add "Why structure matters for AI-generated code" section with audit-units table mapping each post-split file to one reviewable question - §4 4a: expand config/types.rs from description to full execution spec — exact type list, post-split mod.rs content, zero-change submodule guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T which resolves through mod.rs re-exports unchanged), impl-extension pattern already in use documented * docs(roadmap): iteration 14 — editor method map, app helpers, //! queue - §4 4c: config/editor.rs split is now execution-ready — complete 6-file method-to-file table with private helper placement verified (validate_candidate→io.rs, table_path_mut→mod.rs pub(super), auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig) - §4 4e: app/mod.rs split complete — all private helpers mapped (parse_auth_forward_mode_from_cli→config_cmd.rs, workspace_env_scope→workspace_cmd.rs, print_env_table note, remove_data_dir_if_exists→dispatch.rs) - §10 step 5: add //! priority queue — 10 files with draft content, prioritised by cold-landing impact and AI audit risk; selector.rs and instance/mod.rs explicitly document the /→__ invariant * docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed - §4 4d: correct operator_env dependency graph — layers.rs imports both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable resolve_operator_env wrapper at line 797); still a valid DAG - §4 4f: verify trust.rs split safety — FnOnce injection pattern means launch_pipeline.rs has zero dependency on trust.rs; import chain documented; trust bypass audit now requires reading only ~60L - §9 OQ1 closed: op_cache.rs read in full — 4-level structure, per-level invalidation, no TTL/expiry (expiry handled at OpCli subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented * docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence - Fix duplicate Rule 3 section introduced by previous edit; add docker.rs co-location note as third edge case (three edge cases, not two) - Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171 (was listed as 782L); propose 6-file tab-by-tab split with auditability note on the security-adjacent Secrets tab - Add §10 execution-order note: 4a and 4c are independent — editor.rs imports AppConfig via crate::config re-exports regardless of 4a order - Append iteration 16 log entry with confidence table and weakest sections * docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections - Add instance/auth.rs to //! priority queue at #4: four security invariants (0o600 perms, symlink rejection, TOCTOU-safe writes, macOS Keychain) documented in draft //! content - Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types mixed with impl blocks; propose 5-file types/behavior split - Correct stale line counts: render/list.rs 1122→1989 (PR #171 added render_environments_subpanel); state.rs 865→992; priorities upgraded - Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332, mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944 - Renumber //! priority queue to 11 entries (was 10) * docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal - Close OQ2: agent_allow.rs read in full — 55L, correct //! doc, design sound; serves as model for //! priority queue pattern - Add render/list.rs as new Rule 5 violator: 668L production (PR #171 added render_environments_subpanel); propose 3-file split (mod.rs, details.rs, subpanels.rs); note import-path change for agents_block_agent_count - Update §1 module map: agent_allow.rs entry corrected with size/API * docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal - Correct input/editor.rs: 2349L total (was 1304L), 1141L production (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn handle_editor_modal at line 618 was invisible to previous grep pattern; now the largest production file in the codebase; priority → Critical - Correct input/save.rs: 1472L total, 661L production (was 567L) - Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers), secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs - Update key insight paragraph naming input/editor.rs as largest production file * docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict - Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority order; rename existing 4f (launch.rs) → 4g; add circular-import risk note for ManagerStage/EditorState split sequencing - Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid is a tightly-coupled rendering loop); section comments compensate for missing //! - Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs; within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable) * docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections - Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file split (mod.rs + flow.rs + preview.rs); no cross-dependency between flow and preview groups; §10 4f-v updated from Optional to concrete plan - Fix //! queue preamble: "first 10 files" → "first 11 files" - Correct save.rs module map (1418→1472L, correct key exports) and hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save is the Phase 2 partner at ~149L) * docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis - Analyze input/list.rs: 214L production (tests at 215); has //! doc; two focused pub(super) fns; no split needed; Low priority; correct module map - Add mount_info.rs to hot-spot table: 277L production; Low priority; has //! doc; correct module map with 3 public enums + inspect fn - Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists * docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected - Expand audit units table from 8 to 13 entries: add state/types.rs, state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs, input/save/preview.rs — all targeting PR #171 AI-generated console code - Add PR #171 context note linking 5 new entries to AI-generated code concern - Correct input/mod.rs module map: 369L, add InputOutcome enum to exports - Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct * docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed - Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded 1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted - Correct render/mod.rs module map: 421L; FooterItem + palette constants + render_header + centered_rect_fixed added to key exports - Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum) vs "Secrets / Environments" (UI label); /stub qualifier already removed * docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat - Correct too_many_lines count: 13 across 8 → 16 across 11 files (PR #171 added 5 suppressions in console/manager); add full breakdown table; update all 3 occurrences in roadmap - Fix FooterItem PR reference: #165 → #166 (confirmed by git log --follow) - Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description * docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed - Add console/mod.rs to hot-spot table: 406L/307L production (Low); correct module map from ~200 → 406L; note missing //! doc with ConsoleStage design block comment worth promoting - Add op_picker/render.rs to hot-spot table: 865L/545L production (Medium); PR #171 AI-generated; 14 functions in two logical groups (entry/helpers vs level renderers); split into levels.rs proposed - Correct 3 stale ~200L estimates for console/mod.rs across roadmap * docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction - Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High); PR #171 AI-generated; OpPickerState types+behavior split opportunity; has 7-line //! doc; module map split into two rows (mod.rs + render.rs) - Add op_picker/render.rs 2-file split proposal: render.rs (coordinator) + render_pane.rs (pane/level renderers); no cross-dependency confirmed - Correct operator_env.rs total: 1569→2130L (880L production); update 4 occurrences across hot-spot table, ASCII tree, §4 analysis * docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections - Add op_picker/mod.rs formal 3-file split: loading.rs (async load family ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors) - Correct "24 files" → "28+" for 500L threshold count - Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note) * docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis - §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs → render.rs + pane.rs); document impl-extension and import-path caveats - §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files have //! docs, no file exceeds ~350L production; classified as exemplar (not a split candidate); document git_prompt.rs coupling-density justification and input.rs as 28-file false positive (144L production) - §1 module map: expand single file_browser/ row to 5 individual rows with production LOC and dominant concern per file * docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections - §4: Add "Alternative thesis: documentation-first verification" — challenges the two core assumptions behind file splitting (files-as-audit-unit and file-size-as-context-constraint); adds 7-criterion comparison table vs structure-first approach; introduces phased combined recommendation: Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs, zero structural change); Phase 2 = splits only for >600L production files (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K - Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and 8 locations respectively; verified by fresh find|xargs wc -l scan) - §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L), source_picker.rs (244L) — all PR #171 additions with //! docs * docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC - §4 alternative thesis: correct ">600L production → 4 files" claim introduced in iteration 30; re-verified all 9 candidate files via #[cfg(test)] line position; threshold must be >800L to get exactly 4 files (9 exceed 600L); add verification table with test-start lines - Production LOC corrections (5+ locations each): launch.rs 1085→~1077, operator_env.rs 810→~880, app/mod.rs 928→~957, config/editor.rs 503→~584 - §2 OpPicker row: replace vague "no entry yet" with confirmed gap: PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10 named); omits op_picker/, agent_picker.rs, scope_picker.rs, source_picker.rs and pre-dates the manager/ sub-structure split * docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template - §8.1: Add two-tier spec architecture table distinguishing feature specs (public Starlight MDX, user-facing) from behavioral specs (internal docs/internal/specs/, for AI code verification) — resolves contradiction between §4 (which said docs/internal/specs/) and §8.1 (which said "no longer needed; specs are public") - §8.1: Add concrete behavioral spec template for op_picker/ with state machine table and 3 INV invariant entries each with a grep-executable "Verify by:" command; template directly usable for the 3 Phase 1 specs - §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim - Confirmed render/editor.rs ~736L and render/list.rs ~668L production (no interspersed production code — all test blocks follow consecutively) * docs(roadmap): iteration 33 — executive summary, §0 correctness - §0: Add executive summary (~300 words) with core problem, 3-phase recommendation, key counter-argument, and navigation table pointing to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a readability roadmap with no entry-point orientation - §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs current verified size; stale reference was in the first section readers see) - §0 item 3: Add "(selective)" qualifier and explicit note that standard Rust co-locates struct+impl — impl-extension pattern is justified only for files >800L production, not as a universal rule * docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track - §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //! doc, ~1077L production, critical path — all jackin load failures trace here); drop config/editor.rs from Phase 1 (its 963L test suite already serves as behavioral spec — tests are behavioral examples); reduce Phase 1 from 3 specs to 2 specs; add reasoning for the priority ordering - §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling setup) + Track B (Phase 1 behavioral spec authoring); Track B includes specific INV invariants to capture for runtime/launch.rs grounded in reading the actual function structure (step comment positions); adds sequencing rationale: spec must precede structural splits * docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from iteration 34 (inferred from step comment positions) with 5 verified INVs citing exact line numbers: - INV-1: trust gate (line 594) precedes image build (line 736) - INV-2: container name claimed (line 754) between image build and network - INV-3: token verified (line 763) before network creation (line 827) - INV-4: render_exit called at lines 886 AND 890 (all exit paths) - INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup, crash→disarm (explains jackin hardline compatibility) Corrected wrong line number: claim_container_name call is at 754, not 918 (918 is the function definition). Each INV has a grep-executable Verify by. * docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with three concrete options: - Option A: CONTRIBUTING.md rule (necessary but insufficient) - Option B: ci.yml git-diff-scoped shell check (recommended) — only checks files added in the current PR so it doesn't require fixing existing stale entries before merging; greps for module directory name in prose - Option C: Structured TOML module registry (over-engineered for scale) Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts pattern already established in docs/scripts/ * docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture Iteration 36: - §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with 3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry); recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded in existing check:repo-links.ts pattern from docs/scripts/ Iteration 37 (operator directive: greenfield Rust structure): - §4: Add "Greenfield architecture — ideal structure for a growing project" section based on verified cross-module dependency graph (grep iteration 37) - Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0; config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3 - Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config) - Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui, jackin-runtime, jackin-console, jackin-shell + thin binary - Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary - Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration * docs(roadmap): iteration 38 — Rust workspace standards, community evidence Ground workspace recommendation in real-world project research: - ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers - starship and fd-find stay single-crate at 1M+ LOC — no library use case - jackin (43K LOC, no external consumers) maps to starship/fd pattern → single-crate is community-standard; "stay single-crate" recommendation confirmed Update greenfield workspace structure to follow matklad's pattern: - Virtual manifest at root (no [package] in root Cargo.toml) - Flat crates/ directory (not nested); crate names match folder names - version = "0.0.0" for unpublished internal crates - Add inline dep comments to each crate in the ASCII structure Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings + Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22) * docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md §7.9: Reverse previous "reject" recommendation to "adopt" per-directory README.md for major src/ module directories. Rationale: README.md is AI-native — Claude Code, Copilot, Cursor load it automatically on directory entry, giving AI agents orientation before they decide which file to open. PROJECT_STRUCTURE.md being confirmed stale removes the main argument for the "single root file" approach. Add three-layer documentation model table: - README.md: directory orientation (AI + human, on entry) - AGENTS.md: agent workflow rules (root, session start) - CLAUDE.md: @AGENTS.md pointer only — NEVER add content here - //! docs: file-level contracts (when reading/editing) Add specific README.md content targets for 7 directories (src/, src/runtime/, src/console/, src/console/manager/, src/console/widgets/, docs/, docs/internal/). §3 target document shape: Add per-directory README.md to proposed hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md design principle (single-line @AGENTS.md — never duplicate content). * docs(roadmap): internal docs are browsable — unified Starlight site Operator directive: internal docs (architecture, specs, ADRs, roadmap) should be browsable, not hidden filesystem files. They are a different TYPE of docs focused on implementation details and vision, published as a "Developer Reference" section of the Starlight site. §3 target document shape: - docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages) - Browsable at jackin.tailrocks.com/internal/ - Sidebar: "Developer Reference" group (collapsed by default) with sub-sections for architecture, code-tour, contributing, testing, decisions, specs, roadmap - Include astro.config.ts sidebar config snippet §8.1 two-tier spec distinction eliminated: - Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/ - Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location - Both browsable and searchable via Starlight; AI agents can be pointed to URLs §8.3 + §4: - All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/ - ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable) - README.md pointer for src/runtime/ updated to URL reference * docs(roadmap): §11 — modern Rust docs platform (future project) Add §11 capturing the vision for a modern docs.rs alternative with: - rustdoc JSON ingestion → Astro Starlight presentation - MCP server for AI agent queries (Context7 alternative for Rust) - Rust-specific query types: rust_get_context(), rust_find_impls(), rust_search_types() — things Context7 cannot provide - Comparison table vs Context7 - Architecture diagram (ingestion → processing → Starlight + MCP) - Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs - Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional prototype for the platform's processing and presentation layers * docs(roadmap): iteration 39 — update §0, fix stale internal/ paths §0 executive summary: rewrite to reflect decisions from iterations 30-38: - browsable internal docs (jackin.tailrocks.com/internal/) - per-directory README.md adoption (§7.9 reversed) - CLAUDE.md = @AGENTS.md single-line pointer only - greenfield workspace architecture (matklad's virtual manifest pattern) - §11 future project: modern Rust docs platform / Context7-for-Rust - document size 1800+ → 2200+ Fix stale docs/internal/ bare paths not caught by iteration 38 sweep: - Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths - §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path - §10 Track B item 2: op-picker spec path → Starlight MDX * docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit §7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline - Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended) / rustdoc-json crate as Rust binary - Option B recommended: matches existing docs/scripts/ pattern, nightly isolated to separate CI step, zero effect on stable build - Key design: URL at /internal/api/, cross-links to behavioral specs, Starlight unified search, prototype for §11 future project - Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit - Recommend: adopt after Phase 1 //! sprint (value ∝ coverage) §4 Rule 4 pub discipline: replace estimated "50-100 items" guess with verified numbers from iteration 40 grep: - 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files - 0 uses of unreachable_pub lint — no enforcement gate - Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8) - Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn" - Revised scope: ~150-200 mechanical conversions (excludes entry points) * docs(roadmap): split research into 19 actionable items Delete _research_notes.md (no longer needed). Replace 2343L READABILITY_AND_MODERNIZATION.md with: - README.md: index of all 19 items with phase, ordering notes, links - READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L) - items/ITEM-001 through ITEM-019: individual actionable items Items by phase: Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011 Phase 1 (needs confirmation): ITEM-005, 016, 018 Phase 2 (structural splits, confirmation required): ITEM-012..015 Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace) Each item has: summary, key files with line numbers, steps, what needs confirmation, and relevant research backing from the 40-iteration analysis loop. * docs(roadmap): migrate 19 items to Starlight reference/roadmap section Move all codebase health roadmap items from docs/internal/roadmap/items/ (plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/ (MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/). Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to astro.config.ts. Deletes the old items/ directory. Updates the internal README to redirect to the new location. Also adds codebase-readability.mdx — a new overview item that captures the overall readability/restructuring program with a recommended execution order: file splits first, then greenfield workspace, then per-directory README+AGENTS.md, then docs and specs. * docs(roadmap): remove premature internal/roadmap/README.md The internal/ structure doesn't exist yet — it will be created as part of the roadmap items themselves. No need for a redirect stub now. * docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive All content has been distilled into the individual Starlight roadmap pages. The full 2343L research is preserved in git history at commit b7e9fc2. * docs(roadmap): fix check:repo-links errors + remove iteration log - Replace plain code spans with <RepoFile> for validate.rs, mise.toml, Cargo.toml, and op_picker/mod.rs - Remove deleted READABILITY_AND_MODERNIZATION.md reference from codebase-readability.mdx - Delete _iteration_log.md (git history is the archive) * docs(roadmap): fix lychee false-positive link in move-contributing-testing The example redirect text contained a markdown hyperlink to a proposed future file path that doesn't exist yet. Changed to a code span. --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Co-authored-by: Claude <noreply@anthropic.com>
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.
Summary
Implements the spec at
docs/superpowers/specs/2026-04-23-workspace-manager-tui-design.md(PR #164).LaunchStage::Managervariant reachable from the Workspace picker viam. Today's launch path stays keystroke-identical whenmis not pressed.$HOME) → dst-defaults-to-same-as-src modal → workdir-pick-from-mount-dsts → name-with-uniqueness-check → editor with everything populated.scommits viaConfigEditor(from PR 1); Esc with unsaved changes opens Discard/Save/Cancel.TextInputonratatui-textarea,FileBrowseronratatui-explorer,WorkdirPickontui-widget-list. Plus two hand-rolled:Confirm,PanelRain. PR 3's Secrets tab reuses all three third-party-wrapped ones.tui::digital_rain, toast shimmer for save banner, toast auto-expire after 3s.JACKIN_NO_ANIMATIONS=1disables rain transition.This is PR 2 of 3 in the workspace manager TUI series. PR 3 of 3 is #171.
New dependencies
ratatui-textarea0.9.xratatui-explorer0.3.xtui-widget-list0.15.xAll three require ratatui's
unstable-widget-reffeature, enabled inCargo.toml.Scope cuts from the spec (noted in the plan's self-review)
Test plan
cargo test -p jackin --all-targets— green (all 541+ lib tests + integration test + doc tests)cargo clippy -p jackin -- -D warnings— cleancargo fmt --check— cleanmfrom Workspace stage → manager list renders with boot rain reveal. Navigate with ↑↓, Enter edit a workspace → editor tabs work, Tab/Shift-Tab cycles, Esc back. Create flow walks through file browser → dst → workdir pick → name → editor drops in populated → s commits. Delete viad→ Y/N confirm → toast banner.JACKIN_NO_ANIMATIONS=1 cargo run -p jackin— no boot reveal on manager entry; everything else unchanged.mis not pressed.Dependencies
toml_editmigration, merged in feat(config): toml_edit migration (PR 1 of 3) #162): all persisted writes flow throughConfigEditor.🤖 Generated with Claude Code