Files
kandev/docs/specs/quick-terminal/spec.md
T
2026-08-06 11:16:29 +01:00

260 lines
17 KiB
Markdown

---
status: shipped
created: 2026-08-03
updated: 2026-08-05
owner: kandev
---
# Quick Chat and Terminal Tabs
## Why
Quick Chat and Quick Terminal are both short-lived utilities reached from the same navigation
surfaces, but they currently open separate dialogs with different tab and lifecycle behavior. Users
should be able to keep several host terminals beside their utility conversations, switch between
them without losing work, and return to the most recent terminal without managing another window.
## What
- Quick Chat is the single responsive dialog for ordinary chats, configuration chats, and host
terminal tabs. Quick Terminal no longer opens a separate dialog.
- Existing conversation launchers preserve their kind-specific behavior: generic Quick Chat
shortcuts select an ordinary chat, configuration entry points select the workspace's
configuration chat, and either opens its existing setup when no matching conversation exists.
- The existing Quick Terminal launchers use a reuse-or-create policy scoped to the active workspace:
they open the most recently activated terminal tab when one exists, and create the first terminal
tab otherwise.
- The tab-strip plus button opens a creation menu grouped like the task-detail Dockview add menu:
an **Agents** section with **New Agent**, a separator, and a **Terminals** section with
**New Terminal**. Existing tabs remain directly selectable in the tab strip rather than being
duplicated in the creation menu.
- Choosing **New Agent** preserves the current ordinary/configuration setup flow. Choosing
**New Terminal** always creates and activates a distinct host-shell terminal, even when another
terminal exists.
- Chat and terminal tabs share one horizontal tab strip. Conversation ordering and configuration
indicators remain unchanged; terminal tabs are ordered by creation and use a terminal icon with
workspace-local labels such as `Terminal 1`, `Terminal 2`.
- Renameable conversation tabs expose **Rename** from a context menu on desktop right-click and
the equivalent touch long-press gesture. The existing inline editor and backing-task rename
persistence remain unchanged; terminal labels stay fixed.
- Multiple terminal tabs can run concurrently. Input, output, resize, exit, and error state belong
to the selected terminal and must not affect sibling terminals.
- Switching to another tab or dismissing the shared dialog detaches the terminal presentation but
leaves its host-shell session running. Reopening the dialog or reselecting the tab reconnects to
that same session and replays the backend's available recent output.
- Closing a terminal tab stops that tab's host-shell session and removes only that tab. Closing a
chat tab retains the existing Quick Chat confirmation and task-deletion behavior.
- After the active tab is removed, the dialog selects the nearest remaining tab in the same
rendered tab order; if the workspace has no remaining chat, setup, or terminal tab, the dialog
closes.
- A terminal that exits naturally remains as an exited tab until the user closes it. The terminal
shortcut reopens that most recent tab rather than silently replacing it; **New Terminal** is the
explicit way to start another shell.
- Terminal tabs are associated with the workspace from which they were created and are visible,
reusable, and selectable only while that workspace is active. They still run on the Kandev host
and do not acquire a task workspace or repository working directory.
- The expanded desktop sidebar and the tablet/phone Home and Tasks headers retain separate Quick
Terminal and Quick Chat buttons. Both open the shared dialog, select their respective content
kind, and return focus to the launcher when the dialog closes.
- Tablet and desktop use the existing large Quick Chat floating geometry. Phone uses its existing
full-height, dynamic-viewport surface. The tab strip and actions stay fixed while the selected
terminal owns the remaining content region.
- The terminal on **Settings → Agents** retains its existing single-dialog presentation and
stop-on-close behavior.
## Data model
Decision: [ADR-2026-08-05-server-owned-quick-terminal-descriptors](../../decisions/2026-08-05-server-owned-quick-terminal-descriptors.md)
Persisted ordinary and configuration chat sessions keep their existing task/session-backed model.
The shared frontend tab state additionally holds backend-owned terminal descriptors:
| Field | Type | Meaning |
| --- | --- | --- |
| `tabId` | UUID string | Stable client identity and host-shell idempotency key for one terminal tab. |
| `workspaceId` | UUID string | Workspace whose shared dialog owns the tab. |
| `sessionId` | string, optional | Backend PTY session after startup succeeds. |
| `sequence` | positive integer | Workspace-local display order and default terminal label. |
| `status` | `connecting \| running \| exited \| error` | Last observable lifecycle state. |
| `exitCode` | integer, optional | Exit code received while the client was attached. |
| `error` | string, optional | Last start or stream failure displayed in the tab. |
The frontend tracks the last active chat and last active terminal separately so each launcher can
return to its own most recent tab. Terminal descriptors are backend-owned records scoped to the
authenticated user and workspace. They are returned in the Quick Chat boot payload and terminal
resync responses, but never enter task records, conversation rename/delete APIs, or conversation
membership reconciliation. The durable descriptor contains no terminal output and does not make
the PTY itself durable.
The descriptor store uses the following identity and lifecycle rules:
- `tabId` is a UUID generated by the browser and is the idempotency key for one logical terminal
tab. The backend rejects malformed or cross-user updates and never treats it as a shell command,
path, environment variable, or public agent identity.
- `sequence` is allocated by the backend per user and workspace. It is never reused after a tab is
deleted, so labels remain stable across refreshes and devices.
- `sessionId` is the latest live host-shell session association. It is cleared when the backend
can prove that the in-memory PTY no longer exists; a missing session never causes an implicit
replacement.
- `status`, `exitCode`, and `error` are the latest bounded lifecycle snapshot. A live manager entry
is authoritative for `running`; persisted status is used for errors and observed exits.
## API surface
`POST /api/v1/host-shell/start` accepts:
```json
{
"cols": 120,
"rows": 36,
"client_id": "terminal-tab-uuid"
}
```
- `client_id` is an optional UUID. Without it, the endpoint retains the current
singleton/idempotent behavior used by the Agents-page terminal; a present non-UUID value returns
`400 Bad Request`.
- A Quick Terminal tab sends its stable `tabId` as `client_id`. Repeating a start for the same
client ID returns the same running session; a different client ID creates an independent session.
- The response remains the existing host-shell session snapshot with `session_id`, `agent_id`,
`cmd`, `running`, `started_at`, and optional exit fields.
- Stream, status, resize, and stop continue to use
`/api/v1/agent-login/sessions/:sessionID/*`; their payloads do not change.
Quick Terminal descriptors use a separate authenticated API:
- `GET /api/v1/quick-terminal-tabs?workspace_id=<workspaceID>` returns the current user's
descriptors for that workspace, ordered by `sequence`. The boot-state builder uses the same
service so an initial page load and a later resync apply identical reconciliation rules.
- `POST /api/v1/quick-terminal-tabs` accepts `{ "tab_id": "<uuid>", "workspace_id": "<uuid>" }`,
validates workspace visibility, allocates the next sequence, and is idempotent for the same
user/tab ID.
- `PATCH /api/v1/quick-terminal-tabs/:tabID` records the latest session association and bounded
lifecycle fields. A session ID is accepted only when it belongs to the host-shell manager entry
keyed by that tab ID.
- `DELETE /api/v1/quick-terminal-tabs/:tabID` authorizes the row, stops its live host-shell session
when present, and removes the descriptor. Repeating a delete for a missing row is treated as
already closed; a different stop or persistence failure leaves the descriptor available for
retry.
These descriptor routes are distinct from the low-level host-shell and agent-login routes so the
legacy singleton login surface retains its existing behavior and authorization contract.
## State machine
| State | Trigger | Result |
| --- | --- | --- |
| No terminal tab | Quick Terminal launcher | Create and activate one starting terminal tab. |
| Connecting descriptor | Page reload or another client | Reconcile its tab ID with the host-shell manager; reattach if the PTY is live, otherwise mark it unavailable without starting a replacement. |
| Existing terminal tabs | Quick Terminal launcher | Open the dialog on the most recently activated terminal tab. |
| Any shared-dialog state | **New Terminal** | Append and activate a new starting terminal tab. |
| Connecting | Host-shell start succeeds | Store the returned session ID, attach its stream, and mark the tab running. |
| Running | Chat/terminal tab switch or dialog dismissal | Detach the rendered terminal; keep the backend PTY and tab descriptor alive. |
| Detached | Tab selected or terminal launcher used | Reattach to the same session and replay available buffered output. |
| Running or detached | PTY exits | Mark the tab exited when observed; retain it for inspection. |
| Connecting, running, exited, or error | Terminal tab close | Stop the session when one exists, remove the tab, select the nearest remaining same-workspace tab, or close the dialog when none remains. |
## Permissions
Quick terminals retain the existing host-shell permissions and environment of the Kandev backend
process. Workspace association controls descriptor ownership and frontend visibility; it does not
sandbox the shell or grant access to a task worktree. Existing API authentication, WebSocket origin
checks, and Agents-page authorization behavior remain unchanged.
## Failure modes
- A failed start or stream leaves the terminal tab visible with its existing error presentation and
a usable close action. It does not activate or stop sibling tabs.
- Closing a terminal tab while its start request is pending removes the tab and stops the session if
that request later succeeds. Development StrictMode replay uses the stable client ID and cannot
create or stop a sibling session.
- A detached session may exit or reach the existing backend idle/hard timeout. Reattaching to a
session that no longer exists marks the tab exited or unavailable; it does not create a
replacement implicitly.
- A descriptor whose PTY disappeared during a backend restart or while no client was attached is
retained with an exited/unavailable status and a cleared session association. The user must
choose **New Terminal** to create a replacement.
- A successful stop or an already-missing session removes the tab. Any other stop failure is
surfaced and keeps the tab available so the user can retry.
- A descriptor create/update failure is visible on the affected tab and never causes a shell to be
started without a durable tab record. Sibling tabs remain usable.
- Server Quick Chat reconciliation may add or remove persisted conversation tabs, but it must not
discard server-owned terminal tabs or change the active terminal.
- Restoring focus after dialog dismissal must not reopen the sidebar terminal tooltip; pointer hover
continues to show it.
## Persistence guarantees
- Ordinary and configuration chat persistence, cross-device synchronization, renaming, and
expiration remain unchanged.
- Terminal tab descriptors survive page reloads, browser restarts, and access from another client
for the same authenticated user and workspace. The backend is the durable source of descriptor
membership, sequence, lifecycle snapshot, and latest session association.
- Host-shell processes and their rolling output buffers live only in backend memory. Dismissing the
shared dialog does not stop them, but explicit tab close, process exit, the existing idle/hard
timeout, or a backend restart does. A backend restart leaves the descriptor behind and marks it
unavailable until the user explicitly creates a new terminal.
- Reconnection can replay only the backend's existing rolling output buffer; this feature does not
add durable terminal history.
## Scenarios
- **GIVEN** no terminal tab exists in the active workspace, **WHEN** the user activates a Quick
Terminal launcher, **THEN** the shared Quick Chat dialog opens with one starting terminal tab.
- **GIVEN** several terminal tabs exist, **WHEN** the user activates a Quick Terminal launcher,
**THEN** the shared dialog selects the most recently activated terminal without creating another.
- **GIVEN** any chat or terminal tab is active, **WHEN** the user chooses **New Terminal** from the
plus menu, **THEN** a distinct terminal tab and host-shell session are created and activated.
- **GIVEN** the plus menu is open, **WHEN** it renders, **THEN** it shows only **New Agent** under
**Agents** and **New Terminal** under **Terminals**; existing tabs remain in the tab strip.
- **GIVEN** a renameable conversation tab is present, **WHEN** the user right-clicks it or
long-presses it and chooses **Rename**, **THEN** the inline editor opens and a submitted name
continues to persist through the existing conversation rename path.
- **GIVEN** two running terminal tabs, **WHEN** the user executes different marker commands in each
and switches between them, **THEN** each tab displays only its own PTY output.
- **GIVEN** a running terminal tab, **WHEN** the user closes and later reopens the shared dialog,
**THEN** the same tab reconnects to the same session and shows available recent output.
- **GIVEN** a running terminal tab with a persisted descriptor, **WHEN** the user refreshes the page,
**THEN** the Quick Chat dialog restores one terminal tab with the same sequence and reattaches to
the same live session without creating a duplicate PTY.
- **GIVEN** a persisted terminal descriptor whose PTY was lost during a backend restart, **WHEN** the
user opens Quick Chat, **THEN** the tab remains visible as exited/unavailable and no replacement
session starts until **New Terminal** is chosen.
- **GIVEN** the same authenticated user opens another client on the same workspace, **WHEN** its
Quick Chat state loads, **THEN** it receives the persisted terminal descriptor and can reattach to
the live session or see its authoritative exited/unavailable state.
- **GIVEN** a terminal and an ordinary chat both exist, **WHEN** the user alternates the Quick
Terminal and generic Quick Chat launchers, **THEN** each launcher opens the most recently active
matching tab without changing configuration-chat launcher behavior.
- **GIVEN** an active terminal tab with sibling tabs, **WHEN** the user closes it, **THEN** only its
PTY is stopped and the nearest remaining same-workspace tab becomes active.
- **GIVEN** terminal tabs belong to two workspaces, **WHEN** the active workspace changes, **THEN**
the shared dialog never displays or activates the other workspace's terminal tabs.
- **GIVEN** a phone viewport, **WHEN** the user opens the plus menu and creates a terminal, **THEN**
the menu uses the existing touch-safe bottom-sheet treatment and the terminal fills the dialog's
remaining dynamic-viewport region without document horizontal overflow.
- **GIVEN** terminal startup, reattachment, or stop fails, **WHEN** the failure settles, **THEN** the
affected tab remains understandable and dismissible without disrupting chats or sibling terminals.
- **GIVEN** the user opens the terminal on **Settings → Agents**, **WHEN** that dialog closes,
**THEN** its PTY still stops immediately and no Quick Chat terminal tab is created.
- **GIVEN** the shared dialog closes from a sidebar launcher, **WHEN** focus returns, **THEN** the
launcher is focused without an automatically reopened tooltip.
## Out of scope
- Persisting terminal output or keeping a PTY process alive across a backend restart. The durable
descriptor only makes the tab discoverable and reports that its old session is unavailable.
- Synchronizing terminal input or output streams between multiple attached clients beyond the
existing PTY subscription and rolling buffer behavior.
- Task-workspace terminals, repository working-directory selection, or moving terminals between
workspaces.
- Terminal tab renaming, drag reordering, split panes, or a command-palette action.
- Changing Quick Chat task persistence, configuration-chat uniqueness, or ordinary-chat expiration.
- Changing the Agents-page terminal geometry or its stop-on-close lifecycle.
## Implementation plan
[Quick Chat and Terminal Tabs implementation](../../plans/quick-terminal/plan.md)
[Quick Terminal refresh persistence repair](../../plans/quick-terminal-refresh-persistence/plan.md)