Back to Fresh

Orchestrator: New Session — Project Path + Worktree Toggle

docs/internal/orchestrator-new-session-base-path.md

0.3.742.6 KB
Original Source

Orchestrator: New Session — Project Path + Worktree Toggle

Status: Design Document Date: May 2026 Driving feature: Let the user create Orchestrator sessions against an arbitrary project path (any directory, not necessarily the current cwd), and let them choose whether the session gets its own git worktree or runs directly inside the given path.

Motivation

Today the New Session dialog has three inputs — Session Name, Agent Command, Branch — and silently assumes:

  1. The current working directory is a git repository.
  2. The user wants the new session in a fresh git worktree of that repository, forked off origin's default branch.

That works for the common case (one editor instance per repo, one agent per branch) but fails the long tail:

  • Non-git directories. "I want to run an agent in ~/notes/ to refactor my markdown" is currently impossible: git worktree add aborts and the dialog reports not a git repository.
  • Multiple agents on the same worktree. Two agents that share a checkout (e.g. one driving the editor, one running long-running builds) need the same path, not two parallel worktrees.
  • Working from a linked worktree. The current dialog already corrects the slug back to the main worktree — but it doesn't let the user choose a different base repo when they have several checked out side-by-side.
  • Foreign repos. "Spin up an agent against ~/repos/upstream-thirdparty/ to investigate a bug" needs the user to point the dialog at that repo without having to first cd the editor there.

The goal is for users to be able to create sessions regardless of current git state, or even using git at all.

Audit of the current dialog (interactive, tmux capture)

Live exploration of the existing New Session and Open dialogs (2026-05-16, against crates/fresh-editor/plugins/orchestrator.ts HEAD) surfaced a handful of pre-existing issues and one stale artefact that this redesign should fold in.

New Session dialog

Current rendering (tmux capture, 130 cols, fresh git repo with no origin):

┌────────────────────────────────────────────────────────────────────────────┐
│          ORCHESTRATOR :: New Session Dialog :: Review Synthesized          │
│                           Project: tmp/fresh-demo                          │
│                                                                            │
│╭─ Session Name ───────────────────────────────────────────────────────────╮│
││ [(auto-generated)                                                      ] ││
│╰──────────────────────────────────────────────────────────────────────────╯│
│╭─ Agent Command ──────────────────────────────────────────────────────────╮│
││ [terminal                                                              ] ││
│╰──────────────────────────────────────────────────────────────────────────╯│
│╭─ Branch ─────────────────────────────────────────────────────────────────╮│
││ [detecting default branch…                                             ] ││
│╰──────────────────────────────────────────────────────────────────────────╯│
│                                              [ Cancel ]  [ Create Session ]│
│            Tab next  S-Tab prev  Enter advance / act  Esc cancel           │
└────────────────────────────────────────────────────────────────────────────┘
  1. Stale title segment — Review Synthesized. The header currently renders as ORCHESTRATOR :: New Session Dialog :: Review Synthesized (orchestrator.ts:1547). "Review Synthesized" reads as leftover scratch text; it is not surfaced anywhere else and has no documented meaning. Drop it in Phase 1, leaving just ORCHESTRATOR :: New Session.
  2. Placeholder style is washed out under focus. Inactive text inputs render their placeholder italic + dim gray (fg=#505050, italic) — clear "this is a hint" cue. Focused inputs paint a darker focus background and the placeholder loses its dim style: (auto-generated) reads like a literal value when Session Name is the default- focused field. Fix: keep the placeholder's italic + dim foreground over the focused background too (the brackets stay normal-weight so the input outline is still visible).
  3. Session Name placeholder is parenthesised (auto-generated); the others aren't. Agent Command shows terminal, Branch shows HEAD — both unwrapped. The parentheses on Session Name visually compete with a real value the user might type starting with (, and they make the inconsistency more glaring. Drop the parentheses and surface the concrete computed default (session-3, or whatever nextAutoSessionName returns) as the placeholder.
  4. Branch placeholder is HEAD when no origin is configured. Repos without an origin (local-only repos, freshly git init-ed scratch dirs) fall through to HEAD as the base ref. That's a fine behaviour but a confusing placeholder: HEAD looks like a literal ref the user might not want. Render as HEAD (no origin configured) in that case so the reason is visible.
  5. Empty-input cursor is invisible. With no value typed, the focused input shows only the darker background — no blinking caret or glyph inside the brackets. Combined with point 2, an empty focused field with a non-dim placeholder is indistinguishable from a typed value. Once the placeholder is reliably italic + dim the eye can tell the difference, but the design should still call for an explicit cursor glyph at the input's insertion point.
  6. Footer hint Tab next S-Tab prev Enter advance / act Esc cancel doesn't mention ↑↓ history — once history ships, the hint needs the extra entry.

Open dialog (already shipped)

  1. Default focus is now the + New Session button — the focusAdvance(1) removal in this branch took effect: [ + New Session Alt+N ] paints with the focused button chrome (fg=#ffffff bold, bg=#0064c8) on first render. Tab cycle reads new-session → filter → preview-pane buttons.
  2. Footer hint says Enter dive regardless of focus. With focus on the New Session button, Enter opens the new- session form rather than diving into a session — the hint should adapt (Enter activate when focus is on a button) or at least add an Alt+N new entry so the alternative is discoverable from the footer.
  3. Filter input placeholder type to filter… is italic + dim — the convention this design wants the New Session form to match.

Implications for this design

  • The "every default rendered as placeholder" claim in Default state needs to spell out the focused-input case (placeholder keeps italic + dim under focus).
  • Phase 1 picks up two small cleanups it didn't previously enumerate: drop the Review Synthesized header segment, and normalise the Session Name placeholder (no parens, show the concrete computed default).
  • Phase 4 (input history) updates the form footer hint to include ↑↓ history.

Wireframe

Default state — defaults shown as in-input placeholder text

The "Project: <label>" subtitle is gone: the Project Path field itself is the project identifier now, so a static label above it would just duplicate (or worse, drift from) the input.

Every default value is rendered as placeholder text inside its input box (dim foreground, replaced as soon as the user types). The input's actual value starts empty in every field; submitting an empty field substitutes the placeholder's resolved value. This is the same pattern Agent Command and Branch already use today; Phase 1 just extends it uniformly to the new Project Path and Session Name rows. The hint lines under each input ("↑↓ for history", inert-state notes) live outside the box so they don't compete with the placeholder.

All four text inputs (Project Path, Session Name, Agent Command, Branch) carry value history: Up / Down on a focused input scrolls through the values the user has previously submitted in that field, MRU-ordered, much like a shell prompt. An empty value at the bottom of the stack is the "clear" entry. History is per-field, stored globally per user (see Where the multi-window list lives) so it follows the user across projects.

╭─ ORCHESTRATOR :: New Session Dialog :: Review Synthesized ───────────╮
│                                                                      │
│ ╭─ Project Path ───────────────────────────────────────────────────╮ │
│ │ [/home/noam/repos/fresh                                  ]·dim·  │ │
│ ╰──────────────────────────────────────────────────────────────────╯ │
│   ↳ canonical repo root (worktree-resolved). ↑↓ for history.         │
│                                                                      │
│ [x] Create a new git worktree for this session                       │
│      └─ unchecked = run the session directly inside the path above   │
│         (use this for non-git paths, or to share a worktree across   │
│         multiple sessions)                                           │
│                                                                      │
│ ╭─ Session Name ───────────────────────────────────────────────────╮ │
│ │ [session-3                                               ]·dim·  │ │
│ ╰──────────────────────────────────────────────────────────────────╯ │
│                                                                      │
│ ╭─ Agent Command ──────────────────────────────────────────────────╮ │
│ │ [claude                                                  ]·dim·  │ │
│ ╰──────────────────────────────────────────────────────────────────╯ │
│                                                                      │
│ ╭─ Branch ─────────────────────────────────────────────────────────╮ │
│ │ [origin/main                                             ]·dim·  │ │
│ ╰──────────────────────────────────────────────────────────────────╯ │
│   ↳ ignored when "Create a new git worktree" is unchecked            │
│                                                                      │
│                              [ Cancel ]   [ Create Session ]         │
│                                                                      │
│  Tab next · S-Tab prev · ↑↓ history · Space toggle · Enter act · Esc │
╰──────────────────────────────────────────────────────────────────────╯

·dim· marks placeholder rendering: the text inside the brackets is the resolved default, drawn with the ui.placeholder_fg style so it's visibly weaker than typed input. Once the user types, the placeholder vanishes and the typed value takes over in normal foreground.

Inputs stay stacked vertically full-width (not packed side-by-side) so long paths and commands have room to breathe without truncation or horizontal scrolling.

Non-git path — worktree checkbox is disabled

The worktree checkbox is only enabled when the resolved Project Path is inside a git working tree (main worktree or linked worktree). For non-git paths the checkbox is rendered disabled — [·] glyph, dim foreground, unfocusable (skipped by Tab), unresponsive to Space — with an inline hint explaining why. The user cannot toggle it on; submitting the form skips all worktree-related logic entirely.

This is a hard rule rather than a soft default because git worktree add is meaningless against a non-repo: there's no ref to fork from, no .git directory to register the new worktree with, and no branch field to populate. Toggling the control on would only let the user reach a guaranteed-failure submission.

Detection runs asynchronously on every Project Path change (debounced 200ms) via git -C <path> rev-parse --is-inside-work-tree. While the probe is in flight the checkbox stays at its last known state (prevents flicker on each keystroke); it transitions to its new enabled / disabled state when the probe resolves.

Cascading effects when the checkbox is disabled (non-git path):

  • The Branch row is also rendered dim and skipped by Tab — there's no ref to fork.
  • The shared-worktree warning shown on a git path doesn't apply (the session simply runs at the path, like any external directory).
│ ╭─ Project Path ───────────────────────────────────────────────────╮ │
│ │ /home/noam/notes                                                 │ │  ← typed value
│ ╰──────────────────────────────────────────────────────────────────╯ │
│   ↳ not a git working tree. ↑↓ for history.                          │
│                                                                      │
│ [·] Create a new git worktree for this session   (disabled — non-git)│
│                                                                      │
│ ╭─ Branch ─────────────────────────────────────────────────────╮ dim │
│ │ no git — N/A                                            ·dim·│     │  ← placeholder
│ ╰──────────────────────────────────────────────────────────────╯     │

Git path, worktree toggle off — "share-the-checkout" mode

When the user explicitly unchecks the worktree option on a git path, the dialog stays interactive but warns about the implications:

│ [ ] Create a new git worktree for this session                       │
│      ⚠ session will share its working tree with any other sessions  │
│         rooted at this path; concurrent writes may conflict.         │
│                                                                      │
│ ╭─ Branch ─────────────────────────────────────────────────────╮ dim │
│ │                              (shared worktree — N/A)        │     │
│ ╰──────────────────────────────────────────────────────────────╯     │

The Branch field becomes inert in this mode for the same reason as the non-git case: there's no git worktree add to fork off a ref.

Field semantics

Every text input starts with empty value + a placeholder showing the resolved default. Submitting an empty field uses the placeholder's value verbatim. This keeps the form's bracket content honest (what you see typed is what you submitted) and makes ↑↓ history navigation start from a clean slate rather than fighting a pre-filled default.

FieldValue at openPlaceholder (the default that will be used on empty submit)
Project Path""canonical repo root resolved from editor cwd (or cwd verbatim for non-git launches)
Create worktree (cb)enabled iff Project Path resolves to a git working tree; default true when enabled, forced false (and unfocusable / un-toggleable) when disabled
Session Name""next auto-generated name (session-N — computed from the resolved project path)
Agent Command""lastCmd (previous run's command), or terminal if none
Branch""detected default branch (origin/main etc.); inert and (no git — N/A) when worktree=off or non-git path

Input history (Up / Down)

Every text input in the form keeps a per-field history list. The shape (stored under <XDG>/fresh/orchestrator/input_history.json):

json
{
  "version": 1,
  "project_path":   ["/home/noam/repos/fresh", "/home/noam/notes",],
  "session_name":   ["bugfix-1991", "refactor-lsp",],
  "agent_command":  ["claude", "python3 agent.py",],
  "branch":         ["origin/main", "feat/diff-folding",]
}

Behaviour:

  • ↑ / Up on a focused input: walk one entry older into history. The first press saves the current draft (whatever the user has typed but not submitted) at the top of the stack so ↓ can return to it.
  • ↓ / Down on a focused input: walk one entry newer; at the bottom of the stack, restore the saved draft (or empty).
  • Submit appends the value to the front of that field's history (deduplicated — if the value already exists in the list it moves to the front rather than duplicating).
  • Empty submissions (i.e. the user accepted the placeholder) record the placeholder's resolved value, not the empty string, so the next launch surfaces "fresh repo root" / "origin/main" / etc. by name.
  • History is global per user, not per project — the windows.json store is global too (see Where the multi- window list lives), and the user's mental model of "the commands I run" lives with them across projects.
  • Capped at 100 entries per field, MRU-trimmed.
  • The smart-key forwarder used by the Open dialog (where Up/Down on the filter input forward to the list) needs to be disabled for fields with history — Up/Down navigates history here, not a sibling list.

"Canonical repo root" resolution

The pre-filled default for Project Path is derived from the editor's cwd in this order:

  1. git -C <cwd> rev-parse --path-format=absolute --git-common-dirdirname(...) of the result is the main worktree's root, regardless of whether the editor was launched from a linked worktree. This matches the existing logic in submitForm (the slug-resolution path) and protects against nested-orchestrator path blow-up.
  2. If git rejects the cwd (not a working tree), fall back to the editor's cwd verbatim. The placeholder text changes to (non-git — sessions run in-place) so the user knows what they're committing to.

The probe runs at openForm time, asynchronously, the same way the current default-branch probe does. While it's in flight the input renders the cwd as the placeholder; the resolved value replaces the placeholder on completion (the value stays empty either way — the user hasn't typed anything).

Worktree checkbox — interaction model

The checkbox's enabled state tracks the Project Path's git status (see Non-git path — worktree checkbox is disabled). Only when the checkbox is enabled can the user toggle it.

  • Enabled + checked (git path, default): today's behaviour — git worktree add <root> -b <branch> <project-path> rooted at <XDG>/orchestrator/<slug-of-project-path>/<session-name>/.
  • Enabled + unchecked (git path, user opted out): session root is the project path itself. No git worktree add. The session inherits whatever branch the worktree is currently on. Branch field is inert.
  • Disabled (non-git path): the checkbox is rendered [·], unfocusable, with the suffix (disabled — non-git). Submit treats it as unchecked: session root is the project path, no git interaction, Branch field is inert.

When the worktree is shared (unchecked + git path) the session record still goes into the normal persistence layer; it's just that multiple sessions can legitimately resolve to the same root. Reconciliation already keys on session id, not root, so this works without changes to orchestrator_persistence.rs.

Where the multi-window list lives

Decision: global per-user. A single <XDG>/fresh/orchestrator/windows.json holds every orchestrator session the user has ever created, regardless of which project they belong to. Sessions carry a project_path field so the Open dialog can filter / group by project.

Rationale:

  • The whole point of the project-path field is to decouple session creation from the editor's cwd. Persistence should follow the same principle — keying windows.json on cwd or on repo root would re-introduce the coupling the form is explicitly trying to break.
  • A user running an agent in ~/notes/ (non-git) and another in ~/repos/fresh (git) shouldn't have two disjoint stores with different schemas. One store, one schema, sessions filtered by project_path at read time.
  • Users frequently want a cross-project "all my running agents" view — global is the natural home for it.
  • Input history (project paths, agent commands, branch names) already lives globally for the same reason; co-locating windows.json with it keeps the storage model consistent.

File layout

<XDG data>/fresh/orchestrator/
├── windows.json              ← single global store
├── input_history.json        ← per-field MRU history
└── <slug>/                   ← per-project worktrees / artefacts
    └── <session-name>/       ← session root (when worktree=on)

windows.json shape:

json
{
  "version": 2,
  "active": 42,
  "next_id": 87,
  "windows": [
    {
      "id": 42,
      "label": "bugfix-1991",
      "root": "<XDG>/fresh/orchestrator/home_noam_repos_fresh/bugfix-1991",
      "project_path": "/home/noam/repos/fresh",
      "shared_worktree": false,
      "plugin_state": {}
    },
    {
      "id": 43,
      "label": "notes-cleanup",
      "root": "/home/noam/notes",
      "project_path": "/home/noam/notes",
      "shared_worktree": true,
      "plugin_state": {}
    }
  ]
}

Filtering in the Open dialog

The picker bumps to two modes:

  • Project view (default): shows only sessions whose project_path matches the editor's resolved project. The filter input ranks within that subset. Matches today's "sessions for this thing I'm working on" UX.
  • All-projects view (toggle in the filter row, persists per editor instance): shows every session in windows.json, with the project_path rendered as a secondary column so cross-project rows are distinguishable.

The Open dialog's existing filter logic doesn't change — it just operates on a subset.

Concurrent writers

Two editors writing windows.json on quit is a real concern (esp. when both instances watch the same ~/.local/share/):

  • Read-modify-write with an atomic rename: load the current file, splice in this editor's changes (touching only the ids this editor owns), write to windows.json.tmp, rename. Last writer wins for the active and next_id fields, but per-session entries are merged by id so neither editor clobbers the other's sessions.
  • next_id global: kept monotonic by clamping to max(local, on-disk) + 1 at write time. Two editors that both allocate id=87 will see the conflict at the next write boundary; the loser bumps to 88 and rewrites its in-memory state. (In practice id collisions are vanishingly rare because sessions are created interactively.)

This is enough for the common case — a single user across a handful of editor instances. If contention ever becomes a real problem the fragmented layout (<XDG>/orchestrator/sessions/<id>.json) can drop in without schema migration.

Migration from per-cwd persistence

On first launch under v2:

  1. Scan <XDG>/fresh/orchestrator/*/windows.json (the legacy per-cwd files).
  2. For each entry, fill project_path by decoding the directory name (the slug → original path), and shared_worktree = false (the legacy flow always created a fresh worktree).
  3. Merge everything into the new global windows.json; ids collide on the off chance two cwd-keyed files used the same id, in which case the most-recently-modified file wins and the loser gets re-numbered.
  4. Leave the legacy files in place but rename them windows.json.migrated.bak so a downgrade isn't a one-way trip.

The migration runs once and is idempotent — re-running it is a no-op once the v2 file exists.

Behavioural details

Validation order on submit

  1. Trim the Project Path. Substitute the placeholder (canonical repo root or cwd) if empty.
  2. editor.pathExists the result. If missing, render path does not exist in the in-dialog error row and bail.
  3. Re-probe git -C <path> rev-parse --is-inside-work-tree to confirm the worktree-checkbox state matches reality (the live debounced probe might be in flight when the user hits Enter):
    • Path is git, checkbox enabled + checked → existing worktree-add flow runs.
    • Path is git, checkbox enabled + unchecked → use the path as-is for the session root; skip git worktree add; ignore Branch.
    • Path is non-git → checkbox is disabled by definition; use the path as-is, no git interaction. If the probe at submit time disagrees with the UI state (race), trust the probe and proceed without git.
  4. Auto-generate session name if empty (existing logic, but the namespace it scans is now keyed on the resolved project path).
  5. Append the submitted (post-placeholder-substitution) values to each field's input history.
  6. Create the session via editor.createWindow({ root, ... }) exactly as today, and write the new entry into the global windows.json with the resolved project_path and shared_worktree flag.

Backwards compatibility

The form's existing behaviour is the default for a git-cwd launch: the Project Path field pre-fills to the canonical repo root, the worktree checkbox starts checked, and pressing Enter through the form lands on Create with all the same behaviour as today. The new options are additive — users who never touch them see the dialog they're used to (plus the new top-of-form Project Path row).

Focus / tab order in the new dialog

Project Path → Worktree Checkbox → Session Name → Agent Command
            → Branch (skipped when inert) → Cancel → Create
  • Space toggles the checkbox while it has focus.
  • Tab skips the Branch field when it's inert (non-git path or worktree=off).
  • Default focus is the Project Path field (matches the layout's top-to-bottom reading order; the user's first decision is where the session runs).
  • / walk history for the focused input — they no longer forward to anything else.

Out of scope

  • Browsing for the project path with a file picker. The plain text input is enough for the first cut; users paste paths from their shell or terminal, and history covers re-selection. A Browse… button can come later as a small button next to the field.
  • Reusing an existing branch on a non-project-path target (e.g. "create a session in /tmp/scratch but check out branch feat/x"). The current shape — checkbox on / off — doesn't have room for "yes worktree but at this custom root path". If it becomes a real ask, a dedicated Worktree Root row appears below the checkbox.
  • Tracking shared-worktree sessions in the open dialog with a distinct badge. The list already shows the root path; two sessions on the same root render adjacent and look correct. A SHARED badge can come if the visual collision is a real problem in practice.
  • Per-project input history. History is global per user; scoping it to projects would force a more complex storage schema for marginal benefit (and most useful values — agent commands, common branch names — travel with the user).

Implementation phases

Phase 1 — Project Path field, header cleanup, placeholder normalisation

Header / chrome cleanups (one-liners surfaced by the Audit):

  • Drop the :: Review Synthesized segment from the dialog title (orchestrator.ts:1547); render ORCHESTRATOR :: New Session only.
  • Remove the Project: <projectLabel> subtitle row from buildFormSpec. The Project Path input below it is now the authoritative project identifier.

Project Path field:

  • Add the Project Path text input at the top of buildFormSpec, above the Session Name row. value is empty; the resolved default fills the placeholder (same pattern as the existing Agent Command field).
  • Wire the placeholder probe (canonical repo root via git rev-parse --path-format=absolute --git-common-dir, with cwd fallback) into openForm alongside the existing defaultBranch probe. The probe writes to a defaultProjectPath field that the input's placeholder reads, so the probe completing live-updates the placeholder without touching value.

Placeholder rendering (applies to every input):

  • Placeholder text keeps its italic + dim foreground (fg=ui.placeholder_fg, italic) even when the input has focus. The current text widget switches to a darker focus background and loses the italic-dim styling for placeholder text, which makes Session Name's (auto-generated) look like a literal value on first paint. Fix in the text widget renderer (or via a placeholderStyle field on the widget) so the dim style survives focus.
  • Drop the parentheses around Session Name's placeholder. Surface the concrete computed default (session-3, or whatever nextAutoSessionName returns for the resolved project path) instead of the literal string (auto-generated). Async-probe refs/heads/session-N at openForm time, debouncing on every Project Path change.
  • When defaultBranch resolves to HEAD (no origin configured), render the placeholder as HEAD (no origin configured) so the reason is visible rather than looking like the user is being told to type HEAD.
  • Add a visible insertion-point glyph ( or terminal-native caret) inside the focused empty input so the focused-empty state isn't visually identical to a typed value.

Submit:

  • submitForm substitutes each placeholder when its field is empty, then uses the resolved values for the rest of the existing flow.

Phase 2 — Worktree checkbox

  • Add createWorktree: boolean to NewSessionForm, defaulting to true.
  • Render a checkbox widget (new widget kind or styled button with a [x] / [ ] glyph, depending on widget library state).
  • On submit, branch the create path:
    • createWorktree === true → existing git worktree add flow.
    • createWorktree === falseroot = <project path>, skip the worktree-add subprocesses and the branch handling.

Phase 3 — Non-git path detection + checkbox enable/disable

  • Async probe of rev-parse --is-inside-work-tree against the typed Project Path; debounce on every change (200ms).
  • Probe result drives a projectPathIsGit: boolean | null on NewSessionForm (null = in flight).
  • When projectPathIsGit === false:
    • Render the checkbox in disabled style ([·] glyph, dim fg, suffix (disabled — non-git)).
    • Drop the checkbox's key so the host's collect_tabbable skips it — Tab advances straight to Session Name, and Space on a non-focused widget is a no-op.
    • Force createWorktree to false internally so submit takes the no-worktree path regardless of any prior toggle state.
    • Dim the Branch row and drop its key for the same reason.
  • When projectPathIsGit === true: checkbox is enabled, Branch row is interactive, both default-state.
  • When projectPathIsGit === null (probe in flight): freeze the checkbox in its last-known state so keystrokes don't cause flicker. Submit waits on the probe (with a short timeout) before proceeding.

Phase 4 — Input history (Up / Down)

  • Storage: <XDG>/fresh/orchestrator/input_history.json with the schema shown in Input history (Up / Down).
  • Plugin-side state: a historyCursor and draftValue per field on NewSessionForm. Up/Down adjust the cursor and rewrite value from the history list (saving the draft on the first ↑).
  • The smart-key forwarder used in the Open dialog (filter → list) is opt-in via a forwardArrows flag on text({…}). Leave the flag off for the form's inputs so ↑/↓ don't forward.
  • Submit: dedupe-merge the resolved value into the field's history, cap at 100, write the file (best-effort, fire-and- forget).
  • Update the form's footer hint to include ↑↓ history (currently Tab next S-Tab prev Enter advance / act Esc cancel).
  • Open dialog footer (↑↓ nav Enter dive Tab focus Esc close): make the second entry context-sensitive so it reads Enter activate when focus is on a button (today Enter on the focused + New Session button opens the form, not a dive). At minimum, append Alt+N new so the alternative is discoverable from the footer.

Phase 5 — Global windows.json + migration

  • Move persistence from <XDG>/fresh/orchestrator/<encoded_cwd>/windows.json to a single <XDG>/fresh/orchestrator/windows.json.
  • Add project_path and shared_worktree to PersistedWindow. Bump the file version to 2.
  • Migrate on first load: read all legacy per-cwd files, decode each filename → original cwd path, fold sessions into the new store with project_path = decoded_cwd, shared_worktree = false. Rename the legacy files to windows.json.migrated.bak.
  • Add a project_path filter to the Open dialog's list-population step (default: only sessions whose project_path matches the editor's resolved project; the filter input bar gets a new toggle [all projects] to lift it).
  • Concurrent-write safety via atomic-rename read-modify-write.

Phase 6 — Shared-worktree session UX polish

  • Surface a "shared with N other sessions" hint in the Open dialog's preview pane when more than one session resolves to the same root.
  • Decide whether Stop / Archive / Delete on a shared- worktree session means "this row only" or "everything at this root". Leaning: row-only for Stop, but Archive / Delete refuse with a "remove the other sessions on this root first" error.

Phase 7 — Dropdown completion for Project Path + Branch

Two text fields in the form benefit from suggestion-driven typing:

  • Project Path — completing partial paths against the filesystem. The base session form spends most of its time letting the user pick some directory they already have, so completion is the single biggest typing-cost reducer.
  • Branch — completing partial branch names against the repo's local + remote branches and tags (git branch -a + git tag). Same argument: the user knows the prefix of the branch they want, not its full name.

Alternatives considered

#ApproachProsCons
AIn-form inline dropdown (list widget below input)Reuses existing list widget; pure plugin-side change; matches form's visual style.Focus-cycle plumbing: dropdown isn't a tabbable cycle entry, it's "completions for the focused input". Up/Down has to disambiguate history vs. completion.
BSeparate floating popup adjacent to the fieldVisually isolated; easy to dismiss.Z-ordering + cross-panel focus management; two FloatingWidgetPanels alive at once; more state.
CBuilt-in suggestions: string[] on the text widget rendererHost-side dropdown rendering; reusable by every plugin; centralised key handling.Big host change to the widget renderer + key dispatcher; needs a generic "where do completions come from" hook the plugin still has to populate.
DReuse the LSP autocomplete pipelineBattle-tested popup + filter + selection logic.Tightly coupled to editor buffers / language servers; orchestrator's form inputs aren't buffers. Wrong abstraction.

Chosen: A. Smallest blast radius (the whole feature lives in the orchestrator plugin), reuses the list widget that already styles correctly inside floating panels, and the filesystem-listing + git-spawn helpers it needs are already exposed (editor.readDir, spawnCollect).

If a second plugin wants the same affordance later we promote it to C — at that point we know enough about the shapes (anchor handling, async fetch, ordering) to design a generic widget API rather than guessing.

Behaviour

  • Trigger: every text-input change on project_path or branch. Debounced via the same editor.delay-based token scheme the Project Path is-git probe already uses.
  • Project Path candidate source: split typed value into (parent, basename); editor.readDir(parent || ".") then filter to entries whose name starts with basename (case-sensitive — paths are case-sensitive on the kernels we ship on; deferring case-insensitive prefix matching until a user asks).
    • Both files and directories are returned. Directories get a trailing / so the user can see the type at a glance and accepting a directory leaves the cursor primed to keep descending.
  • Branch candidate source: git -C <project> for-each-ref --format=%(refname:short) refs/heads/ refs/remotes/ refs/tags/. Filter by substring (not prefix-only — branch names commonly carry prefixes like feat/ that the user wouldn't type first). De-dup origin/main vs main so the list isn't doubled.
  • Rendering: when the completion list is non-empty AND the input has focus, render a list({ visibleRows: 6 }) immediately below the labeled section. Items use ui.menu_active_bg for the selection, ui.popup_text_fg for body — matches the existing palette popup convention.
  • Keys (only when the dropdown is showing):
    • / — move selection inside the list. Overrides history navigation for the duration the dropdown is visible. (Up/Down on a focused input with NO active completions still walks history, unchanged.)
    • Tab / Enter — accept the highlighted item, replace the input's value, dismiss the dropdown. For Project Path, accepting a directory appends / and re-triggers the probe — the user can keep typing or Tab again to descend.
    • Esc — close the dropdown without changing the input. The dialog itself stays open (Esc-closes-dialog only fires when the dropdown is already closed).
    • Any printable / Backspace — refilter; dropdown follows the new prefix.

Out of scope (this phase)

  • Fuzzy matching. The list is purely prefix / substring; the fuzzy ranker isn't worth the import for the typical 5-20 entries we render.
  • Completion in the Session Name field. Session names are freeform user-chosen strings; no candidate source exists.
  • Multi-line preview (file size, last-modified) inside the dropdown. The orchestrator picker's preview pane already fills that role for selected sessions; the new-session dialog doesn't need it for prospective ones.

Open questions

  • Where does a non-git session's data live on disk? Two natural answers: (a) the path the user gave us (so all artefacts stay with their work); (b) under <XDG>/orchestrator/<slug>/, the same as the git case (clean separation, no surprise dotfiles in the user's folder). Leaning toward (a) — the user explicitly opted out of the worktree, so they probably want their files where they pointed us.
  • Inferring createWorktree from path content. If the user pastes a path that's already a Fresh orchestrator session root (under <XDG>/orchestrator/<slug>/<session>/), the dialog could default the checkbox to off automatically. Worth doing in Phase 3 if the detection is cheap.
  • Path completion. The text input doesn't currently have filesystem-aware completion. Worth a separate proposal — the host already has a fuzzy file picker we could embed, but the UX of "embed a picker in a form field" needs its own design.