Skip to content

Launch in PowerShell, terminal picker, per-session hide - #73

Open
Mr-Godot wants to merge 12 commits into
mainfrom
fix/26-0806-launch-terminal-and-session-hide
Open

Launch in PowerShell, terminal picker, per-session hide#73
Mr-Godot wants to merge 12 commits into
mainfrom
fix/26-0806-launch-terminal-and-session-hide

Conversation

@Mr-Godot

@Mr-Godot Mr-Godot commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #70, closes #71, closes #72.

What changed

Launch opens PowerShell, not cmd.exe (#70). Writes a .ps1 and hosts claude --resume under PowerShell, preferring pwsh.exe and falling back to powershell.exe. -ExecutionPolicy Bypass is required because the generated script is unsigned and lives in %TEMP%.

Terminal picker with a first-run prompt (#72). On the first Launch click with no saved preference, a modal asks which terminal to use, listing only what was detected on that OS. It never picks silently. A gear icon next to Launch and a Settings card change it later.

Design decisions worth knowing:

  • One flat list of terminal profiles, not shell and host as separate settings. Windows Terminal is a host that runs a shell inside it, so treating them independently would mean 5 hosts x 4 shells = 20 mostly-untested combinations. Each pairing is instead its own named profile.
  • Three distinct preference states: key absent (never chose, prompt), "auto" (explicitly chose automatic, never prompt again), or a profile ID. Absent is not a synonym for auto.
  • Keyed per platform, so a synced settings.json does not make two machines overwrite each other's answer and re-prompt forever.
  • Dismissing the dialog cancels the launch. Launching anyway would mean picking a terminal for the user, which is what the requirement forbids.

Hide works per session, not per project (#71). hidden existed only on ProjectMetadataEntrySchema, so hiding hid an entire folder with no way to hide one session. Session hide and project hide are now independent in both directions, with separate non-overlapping counts so the banner cannot double-count, and a hide control was added to the session-detail view where none existed.

Cross-origin POSTs to the launch endpoint are rejected. Pre-existing, surfaced during the #72 design review. /api/launch-session spawns claude --resume --dangerously-skip-permissions and accepted any POST that reached it, so any page in an open browser could drive it against a guessed UUID. Origin is now validated against the request's own Host rather than a hardcoded port, before any body is read.

Security shape of the picker

The stored preference is an allowlisted ID, never a path and never an argument. There is no free-text field anywhere in the feature. The registry is a Map whose lookup rejects any miss, with no regex, trimming, case folding or whitespace splitting. A hand-edited settings.json containing "C:\Users\Public\evil.exe" fails enum parsing and degrades to asking the user rather than spawning. spawn(exe, argsArray) runs with no shell option anywhere in the launch path.

launch-session.ts (UUID regex, cwd sanitization, dead-cwd guard) is unchanged.

Verification

typecheck clean, lint 0 errors, 883 tests passing across 51 files (baseline was 790).

The Origin gate was verified live against the running dev server, not only by unit test: no Origin gives 403, https://evil.com gives 403, the lookalike http://localhost.evil.com:3030 gives 403, and http://localhost:3030 passes through.

Terminal detection was verified live on Windows: all six Windows profiles detected, auto resolving to Windows Terminal with PowerShell 7, Git Bash resolved to its mintty path.

Known gaps

  • No terminal has been spawned end to end by an automated check. Detection is verified; the actual window is not.
  • macOS and Linux registries are complete but unrun, no hardware available. Ghostty and Alacritty are the least certain.
  • E2E tests from section 9.2 of the design were not written.
  • npm run build not run, per the known Node v24 issue noted in CLAUDE.md.

Note for the repo

CLAUDE.md claims architecture boundary tests live in src/__tests__/architecture/ and run in CI. That directory does not exist. Unrelated to this work, but the file is asserting a safety net that is not there.

Design doc: .plans/26-0806-terminal-picker-design.md (gitignored, local only).

…butions, fix search ordering and silent failures

P1 lib/scanner/live-sessions.ts: read ~/.claude/sessions/*.json (pid + status),
   filter by process.kill(pid,0) liveness, memoized 1s. Read-only.
P2 active-detector: drop the mtime heuristic that marked months-dead sessions
   'working'. isActive stays true only for 'working' so waiting sessions do not
   bypass the hidden-project filter, readmit stubs, or pin the 5s poll.
   The 3s active poll no longer runs a full 454-file scan.
P3 stats-parser: cache per-session contributions keyed on a fingerprint that
   includes the subagent directory, killing the 11.1s re-parse per stats request.
   persistSummaryCache now skips the write when nothing changed.
P4 search: stop swallowing provider failures into an empty result, log the real
   driver error, coalesce concurrent refreshes, yield during indexing, and stop
   the retry storm on a rejected search query.
P5 search: order hits newest-first (ORDER BY ts DESC, sid tiebreak), make the
   representative row the most recent matching block so date and snippet agree,
   and apply the limit as a session count server-side.
…lation

G  liveness gaps: the 3s poll now applies the same busy-mtime rule as the scan,
   so a killed-while-busy session can no longer be flipped back to 'working'.
   Forced search refresh no longer no-ops behind an in-flight non-forced pass.
P6 names: drop the ~/.claude/sessions/*.json name tier entirely (it contributed
   163 duplicates, 1 wrong value, 15 derived junk names like 'agents-0a', and 0
   unique correct values). The durable name is the JSONL custom-title. Fixes
   firstUserMessage for string-typed content and skips injected turns.
   SUMMARY_CACHE_VERSION 4 -> 5 to evict poisoned entries.
P7 identity: resolve each project from its recorded cwd into a new realPath
   field (projectPath keeps its lookup meaning), disambiguate same-basename
   projects, flag dead paths instead of hiding them, and return 409 from the
   launch handler rather than spawning into a directory that no longer exists.
P8 UI: shared EmptyState, honest loading affordance that keeps previous rows,
   suppressed stale-filter flash, surfaced search index staleness, and an unset
   theme preference now resolves to dark in both the pre-paint script and the
   provider.

Also fixes a pre-existing data-loss bug: getCacheDir() ignored CLAUDE_HOME, so
the e2e suite wrote to the real ~/.claude-dashboard and pruned the live search
index to its 3 fixture sessions. All dashboard paths now derive from
getDashboardDir(), which honours CLAUDE_HOME.
The Launch button wrote a .bat and hosted `claude --resume` under cmd.exe.
Now writes a .ps1 and hosts it under PowerShell, preferring pwsh.exe when
installed and falling back to powershell.exe.

-ExecutionPolicy Bypass is required because the generated .ps1 is unsigned
and lives in %TEMP%. -NoExit keeps the window open after claude exits.

Shell selection and script generation are factored into powershell-launch.ts
so the terminal picker in #72 can make them configurable.

Closes #70
hidden existed only on ProjectMetadataEntrySchema, so hiding hid an entire
project and there was no way to hide a single session. Adds hidden to
SessionMetadataEntrySchema and filters on it independently of the project
flag in both directions.

Tracks two separate counts so the banner cannot double-count: sessions
inside hidden projects, and sessions hidden one by one.

Also adds the missing hide control to the session-detail view.

No metadata version bump: the field is optional and additive.

Refs #71
Adds a terminal profile picker. On the first Launch click with no saved
preference the app asks which terminal to use, listing only what was
detected on that OS, instead of picking silently.

A profile is one allowlisted ID resolving to a fixed launcher, a fixed
argv template and a script flavor. The stored value is never a path and
never an argument, and the registry is a Map whose lookup rejects any
miss, so a hand-edited settings.json degrades to asking the user rather
than spawning something unexpected.

Preference is keyed per platform so a synced settings.json does not make
two machines overwrite each other's answer.

Also drops resolvePowerShellExe, orphaned by the registry, along with its
shell-interpolated where.exe probe.

Closes #72
/api/launch-session spawned `claude --resume --dangerously-skip-permissions`
for any POST that reached it, so any page in any open browser could drive it
against a guessed session UUID.

Validates Origin against the request's own Host rather than a hardcoded port,
since the port varies (3030 in use, 3000 default, 3001 under e2e). Requiring
Host itself to be loopback also blocks DNS rebinding. Missing and opaque
Origins are rejected: browsers send Origin on every POST, so only non-browser
callers omit it and none exist here.

The check runs before a single byte of body is read.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

PR Size Analysis

  • Total changes: 12124 lines
  • Additions: 11013 lines
  • Deletions: 1111 lines
  • Size: size/large

This label helps reviewers prioritize PRs by size.

)
}

function DetailHideButton({ sessionId, hidden }: { sessionId: string; hidden: boolean }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <react-refresh/only-export-components> reported by reviewdog 🐶
Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the extraHOCs option.

Four independent defects made a renamed session unfindable. Fixed together
because any subset still leaves the user's literal queries returning nothing.

Hidden filtering ran before the search filter, so every session in a hidden
project was unreachable by any query. Hiding now applies only when the query
is empty; revealed rows carry a hidden-project or hidden-session chip and a
count line. SessionFilters dropped showHidden and starFirst on every
keystroke, which destroyed the manual workaround, so navigations now preserve
prior state.

The matcher lowercased the whole query and ran String.includes, so
'vector-crm-v2'.includes('vector crm') was false. Replaced with normalized
term-AND prefix matching: NFKD, combining marks stripped, every non
alphanumeric run folded to one space. Session id keeps a raw substring test
so an 8-char paste still works.

Session names never entered the FTS index and never rendered on result cards,
so the right session could be on screen and unrecognizable. Titles are now
indexed as an additive block_type row and shown on the cards.

Cross-session ordering was pure recency, so a name match could fall past the
result limit. Name matches now outrank body matches.

SCHEMA_VERSION stays at 1: the title row is additive, which avoids a rebuild
of the 214 MB index.

Verified against real data: 'vector crm' returns vector-crm, vector-crm-v2
and vector-crm-v3, and 'brain' surfaces its hidden matches.
The name-match rank was compared before the date, so sessions named Brain,
brain-fix and hermes-brain came back ordered by how closely each name matched
rather than by when they were last used.

The grouping is now binary: a session either matched on its name (tiers 0 to
2) or it did not, and recency decides inside each group. Name matches still
sort above body matches, which was the point of the earlier ranking change.

Verified against real data: query 'brain' now returns brain-fix (Aug 3),
hermes-brain (Jun 17), Brain-3 (Jun 4), Brain (Jun 3), BRAIN (May 19),
BRAIN (May 15), then the body-only matches, each group newest first.
Adds a v1.7.0 changelog section, README sections for choosing a terminal,
hiding one session versus a whole project, and how search now matches
session names and surfaces matches inside hidden projects.

Also removes every em-dash from README, CHANGELOG and the product spec.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant