The toggle sat on its own dedicated row, leaving a large empty band above the
project grid. On desktop (>=769px) float it top-right on the SAME row as the
filter box / launcher header: panels fill from the top (inset 0), launcher-head
reserves right padding for the pill. Mobile keeps the centred stacked toggle to
avoid overlapping the full-width filter on narrow screens. Also reclaim the
hidden key-bar's space on the home view (body.home-open #term { bottom: 0 }).
CSS-only (style.css is served directly). Verified in-browser: toggle and filter
share a row on Projects, no overlap with New/Manage on Sessions.
- Segmented control: replace the two free-standing bordered boxes with a
single rounded track holding two pill buttons (iOS-style), active segment
filled Amber; centred via fit-content + margin auto. Panel top-offset 44→68px.
- Hide the bottom key-bar on the home chooser (it only targets an active
terminal): updateHomeView toggles body.home-open; CSS hides #keybar.
Frontend-only. 415 tests green; both typechecks clean; verified in-browser
(toggle pill renders, keybar hidden on home, reappears when a tab opens).
Once a project/session tab was open, the home chooser (Sessions/Projects
segmented control) was hidden and only reachable by closing every tab — no
way back to the Projects view to open another project.
Add a ⌂ Home button to the tab bar that overlays the chooser on top of the
open tabs (homeForced flag; updateHomeView shows home when no tab OR forced,
hiding the terminal panes). Clicking ⌂ again, clicking a tab (activate resets
homeForced), or opening another session/project dismisses the overlay.
Closing the last tab resets the flag. The button highlights while active.
Tests: +3 (tabs.test.ts). Full suite 415 green; both typechecks clean;
verified in-browser (open tab → ⌂ → Projects panel reachable → ⌂ → terminal).
Prior-session prep for the v0.6 Project Manager, checkpointed on the
v0.6-projects branch as a clean base for the parallel builders:
- P1 (src/types.ts): ProjectInfo + ProjectSessionRef contracts; Config
gains projectRoots/projectScanDepth/projectScanTtlMs/projectDirtyCheck.
- P3 (src/config.ts): parseBool + parseProjectRoots helpers; parse the
4 PROJECT_* env vars with defaults ([homeDir]/4/10000/true).
- UI: public/style.css indigo -> Amber (#e3a64a) accent theme.
- Docs: FEATURE_PROJECT_MANAGER.md (1:N session design), THEME.md,
docs/mockups/ (final-amber.png is the locked visual).
Typecheck (backend + web) green. No behavior change to runtime code yet;
P2/P4/P5/P6 implement the feature on top of this.
Opening the app no longer auto-creates a blank tab or auto-restores/discovers
sessions as tabs. Instead it lands on a launcher (session chooser): a grid of the
host's running sessions as live preview thumbnails — pick one to open (replays its
full scrollback), or '+ New session'. Closing the last tab returns to the chooser.
- public/launcher.ts: mountLauncher() renders the chooser into the terminal area
(live xterm thumbnails via /live-sessions + /preview, 5s refresh), New session +
Manage link
- tabs.ts: drop restore()/syncLiveSessions() auto-tab behavior; constructor lands
on the launcher; rebuild() toggles launcher when tab count is 0; closeTab no
longer spawns a blank tab
- style.css: .launcher chrome (reuses the .mg-* card/grid/thumbnail styles)
Verified: fresh load → 0 tabs, chooser with thumbnails; Open → tab + chooser
hides; close last tab → chooser returns. 228 tests green, tsc + build clean.
The flat session list didn't tell you what each session was doing. Rebuild
/manage.html as a full-page grid where each card renders a LIVE preview
thumbnail — a read-only xterm showing the session's actual current screen,
scaled down like a screenshot — so you can recognize each at a glance.
- RingBuffer.tail(maxBytes): newest scrollback on chunk boundaries (no ANSI/UTF-8
split); GET /live-sessions/:id/preview returns {cols,rows,data} (no attach, so
it doesn't inflate watcher counts or keep sessions alive)
- manage.ts: card grid; per card a scaled read-only Terminal renders CLEAR+tail,
auto-refresh every 4s, click thumbnail/Open to join, Kill / bulk kill
- bundles xterm into manage.js (+manage.css linked in manage.html)
- ring-buffer tail tests
Verified: 3 sessions rendered as recognizable thumbnails (top / git log / ls),
colored, with status/age; click opens. 228 tests green, tsc clean.
The mirror looked broken because a backgrounded tab on one device still pinned
the shared PTY to its (default 80x24) size — so the device actively viewing got
a cramped terminal.
Fix: only an ACTIVELY-VIEWING client votes on PTY size.
- attachWs no longer seeds a default size vote (a join may be a hidden mirror)
- new 'blur' client message + clearClientDims(): a tab going hidden withdraws its
size vote (still mirrors output); show() re-casts it
- PTY size = min cols/rows across clients that have actually reported dims
- session/protocol tests cover hidden-mirror-doesn't-clamp + blur
Session manager (separate page, for the 'too many sessions' problem):
- GET stays; add DELETE /live-sessions/:id and DELETE /live-sessions[?detached=1]
- manager.killById(); LiveSessionInfo gains cols/rows
- public/manage.html + manage.ts: list/open/kill sessions, kill-all / kill-detached,
auto-refresh; 🗂 toolbar button; bundled as a 2nd esbuild entry
Verified: two concurrent clients mirror output + shared input; manage page
lists/kills (3→2→0). 225 tests green, tsc clean.
- tabs.ts: on open, syncLiveSessions() fetches /live-sessions and adds a tab for
each host session not already open → any device sees & mirrors everything
running on the host. Empty storage + no live sessions → one fresh tab.
activeSessionId() + openSession(id) helpers.
- share.ts: 🔗 toolbar button → QR + <origin>/?join=<id> for the active session.
- main.ts: ?join=<id> on load opens/focuses that shared session, then strips the
param. Mount share button (toolbar: 🔍 ⚙ ▦ 🕘 ⌨ 🔗📱).
- style.css: #sharemodal reuses the QR card/backdrop.
Verified in-browser: fresh device auto-discovered the host session and replayed
its scrollback; two concurrent clients (browser + ws) both received live output
(clientCount=2); share modal shows the join URL/QR.
keybar:
- add Ctrl+R (搜历史/\x12), Ctrl+L (重绘/\x0c), Ctrl+D (退出/\x04)
- each key now shows a short function caption under the glyph (self-documenting
on touch where tooltips don't appear); 17 keys total
- test/keybar.test.ts updated to 17 keys
cheat-sheet (new public/shortcuts.ts):
- ⌨ toolbar button opens a grouped reference of common Claude Code shortcuts
(general / editing / multiline / macOS / prefixes), each labeled with what it
does; keys also on the bottom bar are tagged 键栏
- Esc / click-outside to close; styled card with sticky header + accent tags
216 tests green; tsc + build clean.
- src/http/history.ts: listSessions() reads ~/.claude/projects/*/*.jsonl
(mtime-sorted, top 50), parseSessionMeta() extracts cwd + first user prompt
(pure, 4 unit tests); GET /sessions
- terminal-session: opts.initialInput (typed ~700ms after the shell is ready)
- tabs.newTabForResume(cwd,id): new tab in the project dir running
'claude --resume <id>'
- public/history.ts: 🕘 panel listing sessions (project · time · preview) with
Resume buttons
- Verified: /sessions returns 44 real sessions; panel lists project/time/preview.
216 tests green.
- M3: settings.ts (⚙ panel: dark/light/solarized theme + font size A-/A+),
persisted to localStorage; terminal-session.applyTheme; TabApp.applySettings
applies to all + new tabs
- M7: dashboard.ts (▦ overlay listing every tab with connection dot + folder +
Claude status; click a row to focus; live re-render while open);
TabApp.snapshot()/focusTab()
- Browser-verified: light theme renders + persists; dashboard lists tabs and
focuses on click. 208 tests green, typechecks + build ok.
- types/protocol: ClientMessage approve/reject; ServerMessage status.pending
- server: POST /hook/permission HELD until the client decides; approve/reject
ws messages resolve it with {hookSpecificOutput.decision.behavior allow|deny};
5-min timeout + release-on-disconnect fall back to Claude's own prompt
- manager.handleHookEvent threads pending flag
- setup-hooks: PermissionRequest uses the held curl (writes decision to stdout);
status events stay fire-and-forget
- FE: terminal-session approve()/reject() + pending state; tabs approval banner
(Approve/Reject) for the active tab's held request
- Tests: protocol approve/reject; integration ⑧ held→approve→allow. 207 green.
- Browser-verified: banner 'Claude wants to use Bash' → Approve → resolves allow.
- M5: 📱 toolbar button → modal with a client-side QR of location.origin
(shareable when opened via the LAN IP) + localhost tip
- M4: manifest.webmanifest + icon.svg + sw.js (network-first, never intercepts
/term or /hook) + registration in main.ts; apple-touch + theme-color meta
- Browser-verified: QR canvas renders, SW 'controlled', manifest linked,
no console errors. 199 tests green, build ok.
- terminal-session.ts: track SessionStatus (connecting/connected/reconnecting/
exited) + onStatus callback; FIX: wire onTitleChange (onTitle was in opts but
never bound — auto-title was dead)
- tabs.ts: per-tab status dot (color = connection state), unread flag set on
background output + cleared on activate, HTML5 drag-to-reorder (preserves
active tab, persisted)
- style.css: .tab-dot colors, .unread ring, .dragging/.drag-over feedback
- Browser-verified: green dot connected, unread ring after bg output, reorder
AAA|BBB→BBB|AAA persists across reload. 191 tests green, typechecks + build ok.