docs/internal/orchestrator-new-session-base-path.md
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.
Today the New Session dialog has three inputs — Session Name, Agent Command, Branch — and silently assumes:
That works for the common case (one editor instance per repo, one agent per branch) but fails the long tail:
~/notes/ to refactor my markdown" is currently impossible:
git worktree add aborts and the dialog reports
not a git repository.~/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.
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.
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 │
└────────────────────────────────────────────────────────────────────────────┘
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.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).(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.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.▏ 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.Tab next S-Tab prev Enter advance / act Esc cancel doesn't mention ↑↓ history — once history
ships, the hint needs the extra entry.+ 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.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.type to filter… is italic +
dim — the convention this design wants the New Session
form to match.Review Synthesized header segment,
and normalise the Session Name placeholder (no parens, show
the concrete computed default).↑↓ history.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.
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):
│ ╭─ 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
│ ╰──────────────────────────────────────────────────────────────╯ │
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.
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.
| Field | Value at open | Placeholder (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 |
Every text input in the form keeps a per-field history list.
The shape (stored under
<XDG>/fresh/orchestrator/input_history.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:
The pre-filled default for Project Path is derived from the editor's cwd in this order:
git -C <cwd> rev-parse --path-format=absolute --git-common-dir
→ dirname(...) 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.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).
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.
git worktree add <root> -b <branch> <project-path> rooted at
<XDG>/orchestrator/<slug-of-project-path>/<session-name>/.git worktree add.
The session inherits whatever branch the worktree is
currently on. Branch field is inert.[·],
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.
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:
~/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.<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:
{
"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": { … }
}
]
}
The picker bumps to two modes:
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.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.
Two editors writing windows.json on quit is a real concern
(esp. when both instances watch the same ~/.local/share/):
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.
On first launch under v2:
<XDG>/fresh/orchestrator/*/windows.json (the legacy
per-cwd files).project_path by decoding the
directory name (the slug → original path), and
shared_worktree = false (the legacy flow always created
a fresh worktree).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.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.
editor.pathExists the result. If missing, render
path does not exist in the in-dialog error row and bail.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):
git worktree add;
ignore Branch.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.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).
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).↑ / ↓ walk history for the focused input — they no
longer forward to anything else.Browse… button can come later as a small
button next to the field./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.SHARED badge can come if the visual collision is a real
problem in practice.Header / chrome cleanups (one-liners surfaced by the Audit):
:: Review Synthesized segment from the dialog
title (orchestrator.ts:1547); render
ORCHESTRATOR :: New Session only.Project: <projectLabel> subtitle row from
buildFormSpec. The Project Path input below it is now the
authoritative project identifier.Project Path field:
buildFormSpec, above the Session Name row. value is
empty; the resolved default fills the placeholder (same
pattern as the existing Agent Command field).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):
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.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.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.▏ 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.createWorktree: boolean to NewSessionForm,
defaulting to true.checkbox widget (new widget kind or styled
button with a [x] / [ ] glyph, depending on widget
library state).createWorktree === true → existing
git worktree add flow.createWorktree === false → root = <project path>,
skip the worktree-add subprocesses and the branch
handling.rev-parse --is-inside-work-tree against
the typed Project Path; debounce on every change (200ms).projectPathIsGit: boolean | null
on NewSessionForm (null = in flight).projectPathIsGit === false:
[·] glyph, dim
fg, suffix (disabled — non-git)).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.createWorktree to false internally so submit
takes the no-worktree path regardless of any prior toggle
state.key for the same reason.projectPathIsGit === true: checkbox is enabled,
Branch row is interactive, both default-state.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.<XDG>/fresh/orchestrator/input_history.json
with the schema shown in
Input history (Up / Down).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 ↑).forwardArrows flag on text({…}).
Leave the flag off for the form's inputs so ↑/↓ don't
forward.↑↓ history
(currently
Tab next S-Tab prev Enter advance / act Esc cancel).↑↓ 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.<XDG>/fresh/orchestrator/<encoded_cwd>/windows.json to a
single <XDG>/fresh/orchestrator/windows.json.project_path and shared_worktree to
PersistedWindow. Bump the file version to 2.project_path = decoded_cwd,
shared_worktree = false. Rename the legacy files to
windows.json.migrated.bak.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).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.Two text fields in the form benefit from suggestion-driven typing:
git branch -a +
git tag). Same argument: the user knows the prefix of the
branch they want, not its full name.| # | Approach | Pros | Cons |
|---|---|---|---|
| A | In-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. |
| B | Separate floating popup adjacent to the field | Visually isolated; easy to dismiss. | Z-ordering + cross-panel focus management; two FloatingWidgetPanels alive at once; more state. |
| C | Built-in suggestions: string[] on the text widget renderer | Host-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. |
| D | Reuse the LSP autocomplete pipeline | Battle-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.
project_path or
branch. Debounced via the same editor.delay-based token
scheme the Project Path is-git probe already uses.(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).
/ so the user can see the type at a glance
and accepting a directory leaves the cursor primed to
keep descending.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.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.↑ / ↓ — 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).<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.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.