feat(v0.8.9): address all issues labeled v0.8.9

#551 — sidebar filters prior-session agents (from_prior_session)
#552 — status messages prioritise ↑ affordance over /queue
#553 — oversized paste consolidation to @mention file (+uuid suffix)
#523 — release.yml: add if: guard so release job doesn't skip on dispatch
#526 — verify cost_status side-channel is fully wired (already in place)
#554 — mouse/trackpad scroll now sets user_scrolled_during_stream
#522 — set RELEASE_TAG_PAT secret for auto-tag → release trigger
#504 — session-context panel (SidebarFocus::Context, config toggle, default off)
#501 — multi-arch Dockerfile (+BUILDPLATFORM pin) + devcontainer + release CI
#484 — docs/RUNTIME_API.md rewritten against actual runtime_api.rs endpoints
#482 — close v0.8.8 planning tracker

Fixes from review:
- RUNTIME_API.md: corrected endpoints (/v1/...), port (7878), doctor JSON schema (flat)
- Dockerfile: added --platform=$BUILDPLATFORM for native multi-arch builds
- docs/DOCKER.md: removed Docker Hub references (GHCR only)
- sidebar.rs: dropped unused _theme variable
- settings.rs: context_panel default changed to false
- app.rs: paste filename now includes 8-char uuid suffix to avoid collision
This commit is contained in:
Hunter Bown
2026-05-04 00:21:27 -05:00
parent a4219a151b
commit 6ff4db5ba0
11 changed files with 678 additions and 242 deletions
+75
View File
@@ -0,0 +1,75 @@
# Docker
DeepSeek TUI ships an official multi-arch Docker image (amd64 + arm64) on
[GitHub Container Registry](https://github.com/Hmbown/DeepSeek-TUI/pkgs/container/deepseek-tui).
## Quick start
```bash
docker run --rm -it \
-e DEEPSEEK_API_KEY="$DEEPSEEK_API_KEY" \
-v ~/.deepseek:/home/deepseek/.deepseek \
ghcr.io/hmbown/deepseek-tui:latest
```
Images are published to GitHub Container Registry (GHCR) only. Docker Hub
publishing is not currently configured — add a `docker/login-action` step
with Hub credentials to the release workflow if needed.
## Environment variables
| Variable | Required | Description |
|-----------------------|----------|--------------------------------------------------|
| `DEEPSEEK_API_KEY` | yes | DeepSeek API key |
| `DEEPSEEK_BASE_URL` | no | Custom API base URL (e.g. `https://api.deepseek.com`) |
| `DEEPSEEK_NO_COLOR` | no | Set to `1` to disable terminal colour output |
## Volumes
Mount `~/.deepseek` to persist sessions, config, skills, memory, and the offline queue
across container restarts:
```bash
-v ~/.deepseek:/home/deepseek/.deepseek
```
Without this mount the container starts fresh each time.
## Non-interactive / pipeline usage
When stdin is not a TTY, `deepseek` drops to the dispatcher's one-shot mode
(`deepseek -c "…"`). Pipe a prompt on stdin:
```bash
echo "Explain the Cargo.toml in structured English." | \
docker run --rm -i -e DEEPSEEK_API_KEY ghcr.io/hmbown/deepseek-tui:latest
```
## Building locally
```bash
# Single platform (your host architecture)
docker build -t deepseek-tui .
# Multi-platform (requires a builder with emulation)
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t deepseek-tui .
```
## Devcontainer
The repository includes a [`.devcontainer/devcontainer.json`](../.devcontainer/devcontainer.json)
configuration for VS Code / GitHub Codespaces. It pre-installs the Rust toolchain,
rust-analyzer, and the `deepseek` binary. Open the repo in a devcontainer to get a
ready-to-use development environment.
## Tags
| Tag | Meaning |
|------------|--------------------------|
| `latest` | Latest stable release |
| `v0` | Latest v0.x release |
| `0.8.9` | Specific release version |
Docker images are built and pushed automatically when a release tag is pushed
(see [release.yml](../.github/workflows/release.yml)).
+193 -216
View File
@@ -1,172 +1,196 @@
# Runtime API (HTTP/SSE)
# Runtime API & Integration Contract
DeepSeek TUI can expose a local runtime API for external clients:
DeepSeek TUI exposes a local runtime API through `deepseek serve --http` and
machine-readable health via `deepseek doctor --json`. This document is the
stable integration contract for native macOS workbench applications (and other
local supervisors) that embed the DeepSeek engine without screen-scraping
terminal output.
```bash
deepseek serve --http --host 127.0.0.1 --port 7878 --workers 2
## Architecture
```
macOS workbench (or any local supervisor)
├─ deepseek doctor --json → machine-readable health & capability
├─ deepseek serve --http → HTTP/SSE runtime API
├─ deepseek serve --mcp → MCP stdio server
└─ deepseek [args] → interactive TUI session
```
Defaults:
- bind: `127.0.0.1:7878`
- workers: `2` (clamped to `1..8`)
The engine runs as a local-only process. All APIs bind to `localhost` by
default. No hosted relay, no provider-token custody, no secret leakage.
Implementation note:
- The current production runtime lives in `crates/tui` (`runtime_api.rs`, `runtime_threads.rs`, `task_manager.rs`).
- Workspace crate extraction is in progress, but external behavior should be read from the `crates/tui` implementation today.
## Capability endpoint: `deepseek doctor --json`
## Security Model (Local-First)
Returns a JSON object describing the current installation's readiness state.
Suitable for health-check polling from a macOS workbench.
- The server is designed for trusted local use.
- There is no built-in auth, user isolation, or TLS termination.
- Do not expose this API directly to untrusted networks.
- If remote access is required, place it behind your own authenticated reverse proxy/VPN.
```bash
deepseek doctor --json
```
## Runtime Data Model
### Response schema (key fields)
The runtime uses a durable Thread/Turn/Item lifecycle.
| Field | Type | Description |
|---|---|---|
| `version` | string | Installed version (e.g. `"0.8.9"`) |
| `config_path` | string | Resolved config file path |
| `config_present` | bool | Whether the config file exists |
| `workspace` | string | Default workspace directory |
| `api_key.source` | string | `env`, `config`, or `missing` |
| `base_url` | string | API base URL |
| `default_text_model` | string | Default model |
| `memory.enabled` | bool | Whether the memory feature is on |
| `memory.path` | string | Path to memory file |
| `memory.file_present` | bool | Whether memory file exists |
| `mcp.config_path` | string | MCP config file path |
| `mcp.present` | bool | Whether MCP config exists |
| `mcp.servers` | array | Per-server health: `{name, enabled, status, detail}` |
| `skills.selected` | string | Resolved skills directory |
| `skills.global.path` / `.present` / `.count` | — | Global skills dir |
| `skills.agents.path` / `.present` / `.count` | — | `.agents/skills/` dir |
| `skills.local.path` / `.present` / `.count` | — | `skills/` dir |
| `skills.opencode.path` / `.present` / `.count` | — | `.opencode/skills/` dir |
| `skills.claude.path` / `.present` / `.count` | — | `.claude/skills/` dir |
| `tools.path` / `.present` / `.count` | — | Global tools directory |
| `plugins.path` / `.present` / `.count` | — | Global plugins directory |
| `sandbox.available` | bool | Whether sandbox is supported on this OS |
| `sandbox.kind` | string or null | Sandbox kind (e.g. `"macos_seatbelt"`) |
| `storage.spillover.path` / `.present` / `.count` | — | Tool output spillover dir |
| `storage.stash.path` / `.present` / `.count` | — | Composer stash |
- `ThreadRecord`
- `id`, `created_at`, `updated_at`
- `model`, `workspace`, `mode`
- `task_id` (optional durable task link)
- `coherence_state`: `healthy|getting_crowded|refreshing_context|verifying_recent_work|resetting_plan`
- `system_prompt` (optional text)
- `latest_turn_id`, `latest_response_bookmark`, `archived`
- `TurnRecord`
- `id`, `thread_id`
- `status`: `queued|in_progress|completed|failed|interrupted|canceled`
- timestamps, duration, usage, error summary
- `TurnItemRecord`
- `id`, `turn_id`
- `kind`: `user_message|agent_message|tool_call|file_change|command_execution|context_compaction|status|error`
- lifecycle `status`: `queued|in_progress|completed|failed|interrupted|canceled`
- `metadata` (optional tool result metadata; used for task checklist/gate/artifact updates)
### Example
The event log is append-only with global monotonic `seq` for replay/resume.
```json
{
"version": "0.8.9",
"config_path": "/Users/you/.deepseek/config.toml",
"config_present": true,
"workspace": "/Users/you/projects/deepseek-tui",
"api_key": {
"source": "env"
},
"base_url": "https://api.deepseek.com",
"default_text_model": "deepseek-v4-pro",
"memory": {
"enabled": false,
"path": "/Users/you/.deepseek/memory.md",
"file_present": true
},
"mcp": {
"config_path": "/Users/you/.deepseek/mcp.json",
"present": true,
"servers": [
{"name": "filesystem", "enabled": true, "status": "ok", "detail": "ready"}
]
},
"sandbox": {
"available": true,
"kind": "macos_seatbelt"
}
}
```
Session resume note:
- Saved session `system_prompt` currently round-trips as plain text. Structured `SystemPrompt::Blocks` metadata is not preserved when resuming into runtime threads.
## HTTP/SSE runtime API: `deepseek serve --http`
Restart note:
- If the process restarts while a turn or item is `queued` or `in_progress`, the recovered record is marked `interrupted` with an `"Interrupted by process restart"` error instead of remaining stuck in a live state.
```bash
deepseek serve --http [--host 127.0.0.1] [--port 7878] [--workers 2]
```
Approval note:
- `auto_approve` applies to the runtime approval bridge and the engine tool context. When enabled for a thread/turn/task, approval-required tools are auto-approved in the non-interactive runtime path, shell safety checks run in auto-approved mode, and spawned subagents inherit that effective setting for their own tool context.
- If omitted when creating a thread or starting `/v1/stream`, `auto_approve` defaults to `false`.
Defaults: host `127.0.0.1`, port `7878`, 2 workers (clamped 18).
## Endpoints
The server binds to `localhost` by default. Configuration is via CLI flags —
there is no `[app_server]` config section.
### Health and Session
### Endpoints
**Health**
- `GET /health`
**Sessions** (legacy session manager)
- `GET /v1/sessions?limit=50&search=<substring>`
- `GET /v1/sessions/{id}`
- `DELETE /v1/sessions/{id}`
- `POST /v1/sessions/{id}/resume-thread`
- `GET /v1/workspace/status`
- `GET /v1/skills`
- `GET /v1/apps/mcp/servers`
- `GET /v1/apps/mcp/tools?server=<optional>`
Resume session request body (all fields optional):
```json
{
"model": "deepseek-v4-pro",
"mode": "agent"
}
```
Resume session response:
```json
{
"thread_id": "thr_1234abcd",
"session_id": "sess_5678efgh",
"message_count": 24,
"summary": "Resumed session 'Refactor plan' (24 messages) into thread thr_1234abcd"
}
```
### Compatibility Stream (Single Turn)
- `POST /v1/stream`
Backwards-compatible one-shot SSE wrapper. Internally creates an archived runtime thread+turn.
Request body:
```json
{
"prompt": "Summarize recent commits",
"model": "deepseek-v4-pro",
"mode": "agent",
"workspace": ".",
"allow_shell": false,
"trust_mode": false,
"auto_approve": true
}
```
Typical SSE events:
- `turn.started`
- `message.delta`
- `tool.started`
- `tool.progress`
- `tool.completed`
- `approval.required`
- `sandbox.denied`
- `status`
- `error`
- `turn.completed`
- `done`
### Thread Lifecycle
- `POST /v1/threads`
**Threads** (durable runtime data model)
- `GET /v1/threads?limit=50&include_archived=false`
- `GET /v1/threads/summary?limit=50&search=<optional>&include_archived=false`
- `POST /v1/threads`
- `GET /v1/threads/{id}`
- `PATCH /v1/threads/{id}` (currently supports `{ "archived": true|false }`)
- `POST /v1/threads/{id}/resume`
- `POST /v1/threads/{id}/fork`
Create thread request example:
```json
{
"model": "deepseek-v4-pro",
"workspace": ".",
"mode": "agent",
"allow_shell": false,
"trust_mode": false,
"auto_approve": true,
"archived": false,
"task_id": "task_1234abcd"
}
```
### Turn Lifecycle
**Turns** (within a thread)
- `POST /v1/threads/{id}/turns`
- `POST /v1/threads/{id}/turns/{turn_id}/steer`
- `POST /v1/threads/{id}/turns/{turn_id}/interrupt`
- `POST /v1/threads/{id}/compact`
Notes:
- Only one active turn is allowed per thread (`409 Conflict` on overlap).
- `interrupt` returns quickly and marks `turn.interrupt_requested`.
- Terminal turn status becomes `interrupted` only after cleanup completes.
- Manual compaction is exposed as a turn with `context_compaction` item lifecycle events.
- Archiving/unarchiving threads updates persisted thread state and emits `thread.updated`.
### Replayable Events
- `POST /v1/threads/{id}/compact` (manual compaction)
**Events** (SSE replay + live stream)
- `GET /v1/threads/{id}/events?since_seq=<u64>`
Returns SSE replay backlog, then live events for that thread.
**Compatibility stream** (one-shot, backwards-compatible)
- `POST /v1/stream`
SSE payload shape:
**Tasks** (durable background work)
- `GET /v1/tasks`
- `POST /v1/tasks`
- `GET /v1/tasks/{id}`
- `POST /v1/tasks/{id}/cancel`
**Automations** (scheduled recurring work)
- `GET /v1/automations`
- `POST /v1/automations`
- `GET /v1/automations/{id}`
- `PATCH /v1/automations/{id}`
- `DELETE /v1/automations/{id}`
- `POST /v1/automations/{id}/run`
- `POST /v1/automations/{id}/pause`
- `POST /v1/automations/{id}/resume`
- `GET /v1/automations/{id}/runs?limit=20`
**Introspection**
- `GET /v1/workspace/status`
- `GET /v1/skills`
- `GET /v1/apps/mcp/servers`
- `GET /v1/apps/mcp/tools?server=<optional>`
## Runtime data model
The runtime uses a durable Thread/Turn/Item lifecycle.
- **ThreadRecord** — `id`, `created_at`, `updated_at`, `model`, `workspace`,
`mode`, `task_id`, `coherence_state`, `system_prompt`, `latest_turn_id`,
`latest_response_bookmark`, `archived`
- **TurnRecord** — `id`, `thread_id`, `status` (`queued|in_progress|completed|
failed|interrupted|canceled`), timestamps, duration, usage, error summary
- **TurnItemRecord** — `id`, `turn_id`, `kind` (`user_message|agent_message|
tool_call|file_change|command_execution|context_compaction|status|error`),
lifecycle `status`, `metadata`
Events are append-only with a global monotonic `seq` for replay/resume.
### Restart semantics
- If the process restarts while a turn or item is `queued` or `in_progress`,
the recovered record is marked `interrupted` with an `"Interrupted by
process restart"` error.
- Task execution performs its own recovery on top of the same persisted
thread/turn store.
### Approval model
- The `auto_approve` flag applies to the runtime approval bridge and engine
tool context. When enabled for a thread/turn/task, approval-required tools
are auto-approved in the non-interactive runtime path, shell safety checks
run in auto-approved mode, and spawned sub-agents inherit that setting.
- When omitted, `auto_approve` defaults to `false`.
### SSE event stream
The SSE event payload shape:
```json
{
@@ -183,95 +207,48 @@ SSE payload shape:
}
```
Common event names:
- `thread.started`
- `thread.forked`
- `turn.started`
- `turn.lifecycle`
- `turn.steered`
- `turn.interrupt_requested`
- `turn.completed`
- `item.started`
- `item.delta`
- `item.completed`
- `item.failed`
- `item.interrupted`
- `approval.required`
- `sandbox.denied`
- `coherence.state`
Common event names: `thread.started`, `thread.forked`, `turn.started`,
`turn.lifecycle`, `turn.steered`, `turn.interrupt_requested`,
`turn.completed`, `item.started`, `item.delta`, `item.completed`,
`item.failed`, `item.interrupted`, `approval.required`, `sandbox.denied`,
`coherence.state`.
Compaction visibility:
- auto compaction emits `item.started`/`item.completed` with item kind `context_compaction` and `auto=true`
- manual compaction emits the same with `auto=false`
## Security boundary
Coherence visibility:
- `coherence.state` is a machine-readable session-health signal derived from
existing capacity and compaction events. The payload includes `state`,
`label`, `description`, `reason`, and the updated `thread`.
- Normal clients should show the `label` or `description`, not internal
capacity scores or formulas.
- **Localhost only**. The server binds to `127.0.0.1` by default. Set
`--host 0.0.0.0` only when you have a reverse-proxy / VPN that
authenticates — there is no built-in auth, user isolation, or TLS.
- **No provider-token custody**. The server never returns the API key. The
`api_key.source` capability field reports `env`, `config`, or `missing` —
never the key itself.
- **No hosted relay**. The app-server is a local process under the user's
control. There is no cloud component.
- **Capability responses** never leak secrets, file contents, or session
message bodies. They report *metadata*: presence, counts, status flags.
### Background Tasks
## Session lifecycle (native UI supervision)
- `GET /v1/tasks`
- `POST /v1/tasks`
- `GET /v1/tasks/{id}`
- `POST /v1/tasks/{id}/cancel`
| Operation | Endpoint |
|---|---|
| List sessions | `GET /v1/sessions` |
| Get session | `GET /v1/sessions/{id}` |
| Delete session | `DELETE /v1/sessions/{id}` |
| Resume into thread | `POST /v1/sessions/{id}/resume-thread` |
| Create thread | `POST /v1/threads` |
| List threads | `GET /v1/threads` |
| Attach to events | `GET /v1/threads/{id}/events?since_seq=0` |
| Send message | `POST /v1/threads/{id}/turns` |
| Steer | `POST /v1/threads/{id}/turns/{turn_id}/steer` |
| Interrupt | `POST /v1/threads/{id}/turns/{turn_id}/interrupt` |
| Compact | `POST /v1/threads/{id}/compact` |
Tasks execute through the same runtime thread/turn pipeline and include:
- linked `thread_id` / `turn_id`
- runtime event count
- timeline + tool summaries + artifact references
- subordinate checklist state from `checklist_*` / legacy `todo_*` tools
- structured verification gates from `task_gate_run` / completed `task_shell_wait`
- PR attempt metadata and patch artifacts
- guarded GitHub write events
## Compatibility tests
Durable tasks are the model-visible work object. Checklist/todo state is progress
inside the active task/thread, not a parallel task system.
Contract snapshots live in `crates/protocol/tests/`. Run:
Task-aware model-visible tools:
- `task_create`, `task_list`, `task_read`, `task_cancel`
- `task_gate_run`
- `task_shell_start`, `task_shell_wait`
- `pr_attempt_record`, `pr_attempt_list`, `pr_attempt_read`, `pr_attempt_preflight`
- `github_issue_context`, `github_pr_context`, `github_comment`, `github_close_issue`
```bash
cargo test -p deepseek-protocol --test parity_protocol --locked
```
### Automations
- `GET /v1/automations`
- `POST /v1/automations`
- `GET /v1/automations/{id}`
- `PATCH /v1/automations/{id}`
- `DELETE /v1/automations/{id}`
- `POST /v1/automations/{id}/run`
- `POST /v1/automations/{id}/pause`
- `POST /v1/automations/{id}/resume`
- `GET /v1/automations/{id}/runs?limit=20`
RRULE support is intentionally constrained to:
- hourly: `FREQ=HOURLY;INTERVAL=<hours>[;BYDAY=MO,TU,...]`
- weekly: `FREQ=WEEKLY;BYDAY=...;BYHOUR=<0-23>;BYMINUTE=<0-59>`
Automations are persisted under `~/.deepseek/automations` (override with `DEEPSEEK_AUTOMATIONS_DIR`).
Each run is executed as a normal background task and links to task/thread/turn ids.
The same automation manager is exposed to the model through `automation_*`
tools. Create/update/delete/run operations require approval; `automation_run`
and scheduled runs enqueue ordinary durable tasks rather than using a separate
execution path.
## Persistence
Runtime store (default under task data root):
- `runtime/threads/*.json`
- `runtime/turns/*.json`
- `runtime/items/*.json`
- `runtime/events/{thread_id}.jsonl`
- `runtime/state.json` (monotonic sequence)
Task store:
- default `~/.deepseek/tasks` (override with `DEEPSEEK_TASKS_DIR`)
Both runtime and task state are restart-aware.
Queued or in-progress runtime turns reload as `interrupted`; task execution performs its own recovery on top of the same persisted thread/turn store.
This validates that the app-server's event schema hasn't drifted from the
documented contract. CI runs this on every push to `main` and on release tags.