11 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/quick-terminal/spec.md | 2026-08-05 | complete |
Implementation Plan: Quick Terminal Refresh Persistence
Overview
Quick Terminal currently loses its tab descriptors on a full page reload because they exist only in the in-memory Zustand UI slice. This repair adds a backend-owned descriptor lifecycle scoped to the authenticated user and workspace, returns those descriptors through boot and resync, and reattaches a live host-shell session through the existing PTY status/stream routes. The PTY remains ephemeral across backend restarts; a stale descriptor is retained as exited/unavailable and never starts a replacement implicitly.
The work is sequential: the descriptor storage and backend contract must exist before frontend hydration can consume it, and the browser/E2E proof depends on both sides of that contract.
Confirmed root cause
quick-terminal-actions.ts creates QuickTerminalTab descriptors only in
quickChat.terminalTabs. StateProvider creates a new store on reload, while
addQuickChatState restores only task-backed conversation sessions. The host-shell manager may
still hold the detached PTY, but no tabId/sessionId descriptor is booted back into the frontend,
so the shared dialog has no terminal tab to render or attach.
Backend
Descriptor storage and service
- Add a
quickterminalbackend package with model, SQLite repository, service, and focused tests. - Create a durable
quick_terminal_tabstable withtab_id,user_id,workspace_id, nullablesession_id, positive workspace-localsequence, lifecycle status, nullable exit code/error, and created/updated timestamps. Enforce user/workspace/tab and user/workspace/sequence uniqueness, index user/workspace listing, and allocate sequences atomically without reuse. - Resolve the authenticated user from
authn.IdentityFromContext; preserve the synthetic default identity when authentication is disabled. Every read and mutation must authorize the workspace throughtaskService.AuthorizeWorkspaceAccessand scope the repository query by user ID. - Reconcile rows against the
loginpty.Manager: a live host-shell manager entry keyed by the descriptor's UUID supplies the current session and running state; a missing entry clears the session association and yields exited/unavailable without creating a PTY. - Bound and validate all IDs and lifecycle fields at the service boundary. A client may update only its own descriptor and may associate only the manager session whose bounded host-shell key matches that descriptor.
API and boot contract
- Register
GET,POST,PATCH, andDELETE /api/v1/quick-terminal-tabsroutes in a dedicated handler/controller layer.POSTis idempotent for a user/tab UUID;DELETEstops the associated live host shell best-effort before removing the row and treats a missing row as already closed. - Keep
/api/v1/host-shell/startand the existing agent-login session status, stream, resize, and stop routes compatible. Preserve the legacy singleton host-shell behavior whenclient_idis omitted and keep_host_shellas the public agent identity. - Expose the minimal manager lookup/key helper needed by the descriptor service without changing
Manager.Startcallers or exit-callback semantics. - Wire the repository/service into
backendapp/storage.go,backendapp/types.go, route registration, andboot_state.go.quickChat.terminalTabsmust be present for the resolved workspace and use the same reconciliation service as HTTP resync.
Frontend
API and wire mapping
- Add
apps/web/lib/api/domains/quick-terminal-api.tsand the matching HTTP descriptor types. Export list/create/update/delete calls through the API barrel; do not send descriptors through Quick Chat task rename/delete/sync APIs. - Map the backend snake-case descriptor payload into the existing
QuickTerminalTabshape and include terminal descriptors in the bootHydrationStateand workspace resync response.
State and hydration
- Extend the UI slice with server-descriptor adoption/reconciliation actions that preserve local active-kind and last-terminal selection, keep setup/chat placeholders local, and never discard a sibling terminal during conversation resync.
- Make new terminal creation persist the browser-generated UUID before starting a shell, and make lifecycle/session updates durable without replacing a server descriptor with an empty local array. Failed descriptor writes remain visible as an understandable terminal error and do not affect siblings.
- On boot, reload, workspace switch, and socket reconnect, reconcile by
tabIdand workspace. Preserve backend ordering/sequence, restore a livesessionId, and mark stale sessions exited/unavailable without implicit replacement.
Terminal lifecycle and shared dialog
- Update
use-quick-chat-modal.tsand the terminal tab view to persist start/reattach/exit/error state and to delete the server descriptor on explicit close. A stop/delete 404 is already stopped; other failures retain the descriptor and surface the localized error. - Ensure a restored
exited/error/unavailable descriptor does not pass through the PTY view's start-on-mount path. Only a newly createdconnectingdescriptor may start a shell without a session; a restored live descriptor attaches bysessionId. - Preserve the existing detach-on-unmount and late-start ownership guarantees: a detached start that resolves still records its session and a later explicit close stops it; switching tabs or dismissing the dialog never stops it.
- Keep the existing one-dialog desktop/tablet/phone geometry, lazy xterm loading, focus return, fixed terminal labels, and touch-safe controls unchanged.
Tests
- What: descriptor schema, atomic sequence allocation, user/workspace isolation, idempotent
create, live-session reconciliation, stale-session clearing, and close/delete semantics.
Files:
apps/backend/internal/quickterminal/repository/sqlite_test.go,apps/backend/internal/quickterminal/service/service_test.go, andapps/backend/internal/quickterminal/handlers/handlers_test.go. How: real SQLite repository tests plus table-driven service andhttptesthandler tests; include two users, two workspaces, duplicate tab IDs, missing PTYs, and stop/delete 404s. - What: boot payload and workspace resync return persisted terminal descriptors without
erasing conversations or terminals from another workspace.
Files:
apps/backend/internal/backendapp/helpers_test.go,apps/web/lib/state/hydration/hydrator.test.ts, andapps/web/lib/state/slices/ui/quick-chat-sync.test.ts. How: boot fixtures with a live and stale descriptor, then pure state reconciliation tests with empty server conversation lists and multiple workspaces. - What: browser API mapping and store actions persist create/update/remove while retaining
active terminal selection and preventing an exited descriptor from starting a replacement.
Files:
apps/web/lib/api/domains/quick-terminal-api.test.ts,apps/web/lib/state/slices/ui/quick-chat-actions.test.ts,apps/web/components/quick-chat/use-quick-chat-modal.test.ts,apps/web/components/quick-chat/quick-terminal-tab-view.test.tsx, andapps/web/components/settings/pty-terminal-view.test.tsx. How: mocked API, xterm, WebSocket, ResizeObserver, and StrictMode tests; cover reload reattachment, stale-session no-start, detached late start followed by close, natural exit, 404 close, and sibling isolation.
E2E Tests
- Scenario: GIVEN a running terminal with marker output, WHEN the user reloads the page and
opens Quick Chat, THEN exactly one terminal tab with the same sequence reconnects to the same
PTY and replays the marker without a second shell.
File:
apps/web/e2e/tests/terminal/quick-terminal.spec.ts. What to verify: shared dialog identity, tab count/label, marker output, no standalone terminal dialog/footer, and cleanup that explicitly closes every surviving terminal infinally. - Scenario: GIVEN a persisted descriptor after the backend has restarted, WHEN the user opens
Quick Chat, THEN the tab is visible as exited/unavailable and New Terminal is required for a
new shell.
File:
apps/web/e2e/tests/terminal/quick-terminal.spec.ts(or a focused backend fixture helper if a process restart cannot preserve the test database). What to verify: accessible lifecycle status, no implicit new PTY, explicit replacement, and workspace filtering. - Scenario: GIVEN a phone viewport, WHEN the user reloads and reopens the shared surface, THEN
the persisted terminal remains contained in the dynamic viewport and the terminal strip/menu
retains its touch-safe controls and zero document horizontal overflow.
File:
apps/web/e2e/tests/terminal/mobile-quick-terminal.spec.ts. What to verify: same-session marker, terminal panel height, 44px controls/menu rows, focus/ dismissal, and teardown of all surviving tabs.
Verification Results
- Backend:
go test ./internal/backendapp ./internal/quickterminal/... ./internal/agent/loginpty/... -count=1passed (279 test cases/subcases);make -C apps/backend lintpassed with 0 issues. - Frontend: focused Vitest passed (9 files, 124 tests); web typecheck, changed-file ESLint, i18n ratchet, and i18n checks passed. The i18n check retained its existing advisory 657 zh-cn catalog issues and 5 orphan English keys.
- E2E: managed desktop Chromium
quick-terminal.spec.tspassed 2/2; managed Pixel 5mobile-quick-terminal.spec.tspassed 1/1. Both runs included reload/reattachment and explicit terminal cleanup.
Implementation Waves And Parallel Candidates
Wave 1:
Wave 2:
Wave 3:
Wave 4:
All tasks are sequential. The backend schema, host-shell manager lookup, boot payload, frontend hydration, and E2E fixtures share the same lifecycle contract, so no task is parallel-safe.
Risks
- A durable row can outlive its in-memory PTY. Reconciliation must clear stale session IDs and must not turn a refresh into an implicit new shell.
- Create, start, detach, late completion, exit, and explicit close can race. Stable UUIDs and server-side idempotency must prevent duplicate sessions and orphaned descriptors.
- Multiple authenticated clients can mutate sibling tabs concurrently. Repository updates must be row-scoped and sequence allocation must not overwrite unrelated descriptors.
- Host-shell permissions remain those of the backend user and are not task-worktree isolated.
- The existing rolling output buffer remains bounded and non-durable; a refresh after output has aged out may restore the tab without restoring all prior text.