@fishwithcat/dsh-tui-app 0.0.0-stage → 0.2.2-alpha.3.fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +79 -0
- package/README.md +245 -2
- package/README.zh.md +246 -0
- package/cordis.patch.yml +67 -0
- package/lib/index.js +7019 -0
- package/lib/startup.js +54 -0
- package/lib/types/ansi.d.ts +109 -0
- package/lib/types/app.d.ts +480 -0
- package/lib/types/clipboard.d.ts +61 -0
- package/lib/types/commands.d.ts +101 -0
- package/lib/types/config.d.ts +35 -0
- package/lib/types/diagnostics.d.ts +23 -0
- package/lib/types/diff.d.ts +37 -0
- package/lib/types/images.d.ts +37 -0
- package/lib/types/index.d.ts +28 -0
- package/lib/types/interactions.d.ts +132 -0
- package/lib/types/measurement.d.ts +18 -0
- package/lib/types/session.d.ts +151 -0
- package/lib/types/shell.d.ts +111 -0
- package/lib/types/startup.d.ts +34 -0
- package/lib/types/subagents.d.ts +75 -0
- package/lib/types/tool-view.d.ts +39 -0
- package/lib/types/transcript.d.ts +216 -0
- package/lib/types/views.d.ts +572 -0
- package/lib/types/watch/client.d.ts +70 -0
- package/lib/types/watch/history.d.ts +25 -0
- package/lib/types/watch/protocol.d.ts +146 -0
- package/lib/types/watch/record.d.ts +50 -0
- package/lib/types/watch/server.d.ts +172 -0
- package/lib/types/watch/viewer.d.ts +128 -0
- package/locale/de.json +6 -0
- package/locale/en.json +6 -0
- package/locale/es.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/pt.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +110 -4
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/bundle/tui-app/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 5ef521aafc9f2218
|
|
7
|
+
zh: 1c48c446b495c0c5
|
|
8
|
+
/deepseek-ai-dsh-tui-app:
|
|
9
|
+
en: 6a23ca8f9a47405e
|
|
10
|
+
zh: b64330cf6abe3c84
|
|
11
|
+
/deepseek-ai-dsh-tui-app/summary:
|
|
12
|
+
en: ac1ac8f5faa37243
|
|
13
|
+
zh: a1315993460c7fd3
|
|
14
|
+
/deepseek-ai-dsh-tui-app/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/deepseek-ai-dsh-tui-app/use-this-package:
|
|
18
|
+
en: 88c14b005e03bf78
|
|
19
|
+
zh: 0b9ad8bc5a690a3c
|
|
20
|
+
/deepseek-ai-dsh-tui-app/use-this-package/composing-prompting-and-interrupting:
|
|
21
|
+
en: 34da0bc57f324c76
|
|
22
|
+
zh: 7ca36d67e490aeaf
|
|
23
|
+
/deepseek-ai-dsh-tui-app/use-this-package/watching-a-subagent:
|
|
24
|
+
en: 06e52609345e4e25
|
|
25
|
+
zh: fde80695a5359b03
|
|
26
|
+
/deepseek-ai-dsh-tui-app/use-this-package/commands:
|
|
27
|
+
en: 39e96c52dda57e65
|
|
28
|
+
zh: d2aade8d44940539
|
|
29
|
+
/deepseek-ai-dsh-tui-app/use-this-package/local-shell-commands:
|
|
30
|
+
en: b410979abef87262
|
|
31
|
+
zh: 8951a763c4054fc5
|
|
32
|
+
/deepseek-ai-dsh-tui-app/use-this-package/settings:
|
|
33
|
+
en: 091f286d20d6c617
|
|
34
|
+
zh: 626a2a502fe56357
|
|
35
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation:
|
|
36
|
+
en: a7daa7d1183a433f
|
|
37
|
+
zh: 8abbd0d3ec08a8d6
|
|
38
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation/rendering:
|
|
39
|
+
en: 5acabadd3a7a501d
|
|
40
|
+
zh: de3908c848d3d1fa
|
|
41
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation/interaction-seams:
|
|
42
|
+
en: ad6ba4563696862a
|
|
43
|
+
zh: 469b082552af2e27
|
|
44
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation/clipboard-images:
|
|
45
|
+
en: 56fdbc5555bed800
|
|
46
|
+
zh: 47631ee2be590508
|
|
47
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation/patch-surface-over-base:
|
|
48
|
+
en: d36899ca12702771
|
|
49
|
+
zh: ef9c1a87b54bc693
|
|
50
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation/source-map:
|
|
51
|
+
en: bcc8357828fac133
|
|
52
|
+
zh: 8da9879fda2c2fa7
|
|
53
|
+
/deepseek-ai-dsh-tui-app/understand-the-implementation/invariant-ownership:
|
|
54
|
+
en: a6fef7da2427069c
|
|
55
|
+
zh: f8d93f6061eac13b
|
|
56
|
+
/deepseek-ai-dsh-tui-app/further-exploration:
|
|
57
|
+
en: c0641a758f0b6fc2
|
|
58
|
+
zh: ed75683ed4106e50
|
|
59
|
+
/deepseek-ai-dsh-tui-app/model-experience:
|
|
60
|
+
en: 215e7ba838619b7b
|
|
61
|
+
zh: a311da3843709f90
|
|
62
|
+
/deepseek-ai-dsh-tui-app/model-experience/interactive-coding-session:
|
|
63
|
+
en: 75fef2974a28ed59
|
|
64
|
+
zh: 2a8e0a3bf5701745
|
|
65
|
+
/deepseek-ai-dsh-tui-app/model-experience/interactive-coding-session/what-the-model-sees:
|
|
66
|
+
en: 1f37caa3b5a79ed2
|
|
67
|
+
zh: bfff24eb1b72fae3
|
|
68
|
+
/deepseek-ai-dsh-tui-app/model-experience/interactive-coding-session/token-effect:
|
|
69
|
+
en: 7260a205dc748857
|
|
70
|
+
zh: f3fd6cb26fff8aa8
|
|
71
|
+
/deepseek-ai-dsh-tui-app/model-experience/interactive-coding-session/kv-cache-effect:
|
|
72
|
+
en: 113a1ddb9516cf95
|
|
73
|
+
zh: d852ea1f4d1dc176
|
|
74
|
+
/deepseek-ai-dsh-tui-app/known-limitations-and-deferred-work:
|
|
75
|
+
en: 451f6913260e119c
|
|
76
|
+
zh: 0b6fa8bbdb79f07a
|
|
77
|
+
/deepseek-ai-dsh-tui-app/known-limitations-and-deferred-work/dev-note:
|
|
78
|
+
en: e67f633dc6fe7773
|
|
79
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,246 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Interactive terminal UI for dsh: one Agent, one session, driven in-process from a full-screen terminal surface, for users working over SSH or preferring a terminal to the browser."
|
|
3
|
+
kind: "package-bundle"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @deepseek-ai/dsh-tui-app
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-tui-app` is dsh's interactive terminal surface. Run `dsh` and one Agent starts in the current directory with the shared model, sandbox, and approval defaults, and the base tools plus `apply_patch` — but rendered as a full-screen terminal application: a scrolling transcript, a composer above a pinned footer, and status lines that report the workspace, context occupancy, routed model, token throughput, total tokens, cache-hit rate, and the delegated-subagent count. A loopback watch service lets another terminal read a local subagent's transcript and live stream without a second Agent. The main boundary: one session per invocation, with no browser or file sidebar.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Open the terminal UI from the directory that should be the workspace:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
dsh
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`dsh` with no profile boots this bundle; `dsh --profile tui` names it explicitly and takes the same flags. `DSH_DEFAULT_PROFILE` selects a different default for a bare invocation (`DSH_DEFAULT_PROFILE=web dsh`), and an empty value restores the upstream requirement that every launch name its profile. The app refuses to start without an interactive terminal, so a piped or redirected invocation fails loudly instead of waiting for keys it cannot read.
|
|
35
|
+
|
|
36
|
+
### Composing, prompting, and interrupting
|
|
37
|
+
|
|
38
|
+
Type a prompt and press Enter. While the Agent works, the composer switches to steering: an Enter submits text the running turn consumes at its next step, and Esc interrupts the turn together with every live subagent descendant the session delegated to. Delegated work outlives the Agent's own turn, so the footer keeps reporting `running` and the composer keeps offering the cancel keys while a subagent is still inside a turn. A manual `/compact` runs outside the turn lifecycle, so while the summary is generated the footer replaces `idle` with `compacting`, and then reports the settled result as a notice. PageUp/PageDown, the mouse wheel, and terminal search scroll the transcript without leaving the app; the status bar reports when the view has scrolled away from the newest row.
|
|
39
|
+
|
|
40
|
+
In alternate-screen mode, dragging to select text copies it automatically on release. Local macOS sessions use `pbcopy`, including in Terminal.app; a failed write displays `Copy failed`. Other platforms and SSH sessions require terminal support for OSC 52 clipboard writes, whose success cannot be confirmed. Command+C copies the selection again when the terminal forwards that key. If the terminal intercepts Command+C, use its native selection gesture (often Option-drag on macOS), then Command+C; native selection also handles copying in inline mode. Ctrl+C retains its interrupt/exit behavior.
|
|
41
|
+
|
|
42
|
+
Press Ctrl+V to attach the image on the system clipboard to the draft. The composer shows every held image as an `[Image #1]` marker, which behaves like typed text: moving or deleting a marker moves or drops its image. The paste reads the clipboard directly, so the terminal's own text paste keeps its usual key. While that read is in flight the footer reports `pasting image…`, so a slow platform reader never looks frozen.
|
|
43
|
+
|
|
44
|
+
Press Shift+Tab to toggle plan mode for the session's Agent. While plan mode is on, the footer marks it with `plan` beside the lifecycle state and the empty composer lists the key. A toggle made while a turn is running applies from the next step, exactly as `/plan` does; a deployment that mounts no plan mode reports it instead of changing the session. When the agent finishes planning it presents the plan through `exit_plan_mode`, and the review shows that plan as markdown above the Approve / Keep planning choice, taking the rows above the pinned composer and footer so a long plan reads on any terminal size, scrollable with the arrows, PageUp/PageDown, and the wheel, because the plan-mode policy routes the plan through the tool rather than a plain reply. Choosing Keep planning leaves plan mode active and hands the turn back, so the agent waits for your next message instead of revising the plan immediately.
|
|
45
|
+
|
|
46
|
+
| Key | Action |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `Enter` | Submit the prompt, or steer the running turn |
|
|
49
|
+
| `Esc` | Interrupt the running turn and the session's live subagents; at a prompt, skip the question |
|
|
50
|
+
| `Shift+Tab` | Toggle plan mode |
|
|
51
|
+
| `Ctrl+V` | Attach the image on the system clipboard to the draft (`Alt+V` on Windows and WSL, where the terminal owns Ctrl+V) |
|
|
52
|
+
| `Ctrl+C` | Cancel the running turn and the session's live subagents, or stop a local `!` command; at a prompt, cancel the whole ask; with neither working, exit |
|
|
53
|
+
| `Ctrl+D` | Exit |
|
|
54
|
+
| `PageUp` / `PageDown` | Scroll the transcript, or a question's overflowing detail |
|
|
55
|
+
| `Up` / `Down` | Scroll a question's overflowing detail; otherwise move its picker |
|
|
56
|
+
| `Left` / `Right` | Move a picker's selection while a question's detail owns the arrows |
|
|
57
|
+
| `Space` | Check or uncheck an option in a multi-select question |
|
|
58
|
+
| `Ctrl+L` | Repaint from scratch |
|
|
59
|
+
|
|
60
|
+
Exiting — Ctrl+C with nothing running, Ctrl+D, or `/quit` — restores the terminal and prints the session id as a `dsh-tui --resume <session-id>` command, so a later invocation continues the same conversation. Resuming — that command at startup, or `/resume` in the composer — restores the stored conversation into the transcript before the composer accepts the next prompt, so the earlier prompts, replies, and Tool rows are back on screen instead of only their effects on the model. The `/resume` picker and `/sessions` label each stored session with its title — the leading words of the first human message, a generated title, or an explicit rename — beside its id, start time, and workspace. The hint is printed only where the deployment mounts a session-persistence backend; without one there is nothing for `--resume` to open.
|
|
61
|
+
|
|
62
|
+
### Watching a subagent
|
|
63
|
+
|
|
64
|
+
When a local subagent starts, the TUI publishes a loopback connection record and then prints a watch command as an ordinary notice:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
· watch: dsh-tui watch <session-id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `--watch <session-id>` application flag is equivalent.
|
|
71
|
+
|
|
72
|
+
`/subagents` opens a pinned panel listing the session's delegated descendants through the subagent service, each with its activation state and the exact watch command. The read reaches stored children as well as resident ones, and a child catalog it cannot read appears as a diagnostic row. The panel refreshes when a catalog fact or lifecycle edge lands, so children that start or settle while it is open join or leave the list; the list never writes a transcript row, and the main Agent keeps streaming underneath without moving it. Up and Down or PageUp and PageDown scroll a list taller than the panel, and Enter, Esc, or Ctrl+C closes it without cancelling the Agent.
|
|
73
|
+
|
|
74
|
+
Run that command in another terminal to follow the child. The viewer is read-only: it has no composer and does not send prompts, approve Tools, or cancel the child. It shows the committed transcript plus the text, reasoning, Tool calls, and approval audit rows currently streaming, so opening it in the middle of a response shows the prefix already generated. PageUp/PageDown, the mouse wheel, selection copy, the palette, and the configured screen mode behave as they do in the interactive surface. Esc, Ctrl+C, and Ctrl+D only leave the viewer. Completing a model response keeps the connection open. A finished execution keeps its screen and the stream stays open for a later execution of the same child; a lost connection keeps the last trusted screen and reports the disconnect instead of reconnecting.
|
|
75
|
+
|
|
76
|
+
A blank line and a horizontal rule separate the transcript from the footer. The footer names the child session id, its execution state, and the connection state: `online`, `history`, or `disconnected`. Its second line shows available context occupancy, decode throughput, total billed tokens, and cache-hit share from the child's committed events, in both online and history modes; disconnecting preserves the last figures. When no live service is published, or the published service is gone, the viewer opens the stored log through the persistence read handle and marks `history`; it takes no write lock, restores no Agent, and issues no model request. A bad token, an incompatible protocol, or a corrupt record is reported rather than silently downgraded to history. Both terminals must use the same user, the same `$DSH_HOME`, and compatible TUI persistence settings.
|
|
77
|
+
|
|
78
|
+
### Commands
|
|
79
|
+
|
|
80
|
+
A line beginning with a known `/command` runs that command instead of reaching the model. Other slash text reaches the Agent unchanged: `/ponytail full` invokes an installed user-invocable skill through the shared skill loader, while unknown skill names remain ordinary text. App commands and registered commands take precedence over skills with the same name. The app owns the session commands below; every other registered command — `/compact`, `/goal`, `/plan`, and deployment-provided commands — is discovered from the command registry and dispatched against the live Agent.
|
|
81
|
+
|
|
82
|
+
| Command | Effect |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `/help` | List the app's commands and every registered command |
|
|
85
|
+
| `/new` | Flush the session, start a fresh one, then close the previous |
|
|
86
|
+
| `/sessions` | List stored sessions by title, newest first |
|
|
87
|
+
| `/subagents` | Open a pinned, live panel of delegated children with their activation state and watch command |
|
|
88
|
+
| `/resume [id]` | Resume a stored session by id, or choose one from a picker; naming the live session keeps it |
|
|
89
|
+
| `/model [provider/model]` | Choose a model from the live catalog, or switch directly |
|
|
90
|
+
| `/effort [id]` | Choose the routed model's declared reasoning effort, or switch directly |
|
|
91
|
+
| `/questions` | Answer a question whose foreground window closed, from the picker of calls still waiting |
|
|
92
|
+
| `/quit` | Exit |
|
|
93
|
+
|
|
94
|
+
### Local shell commands
|
|
95
|
+
|
|
96
|
+
A line beginning with `!` runs locally instead of reaching the Agent: `!git status` executes through the mounted shell executor under the same sandbox policy as the agent's `bash` tool, without an approval prompt. Each line is a fresh shell in the session's working directory, so `cd` and exported variables do not carry to the next line. The command asks for the executor's upper bound (600 seconds by default), and output beyond the per-stream cap stays a tail whose spill file the row names. The footer reads `● shell` while it runs.
|
|
97
|
+
|
|
98
|
+
The row is local only. Its command and output never enter the session log, are never sent to the model, and disappear on `/new`, `/resume`, and exit, so a later turn cannot cite them. `\!ls` sends the literal text `!ls` to the Agent instead.
|
|
99
|
+
|
|
100
|
+
One local command runs at a time: while it runs, the composer refuses prompts and further `!` lines and keeps the draft, and while a turn or a compaction runs a `!` line is refused the same way. Ctrl+C stops the running command, keeps the output it already produced, and leaves the turn alone; the closing line reports `[exit code: N]`, `[cancelled]`, or `[timed out after 600s]`, and a sandbox denial names the mode that blocked it. Slash commands still run while a local command works. A deployment that mounts no shell executor refuses `!` lines and says so.
|
|
101
|
+
|
|
102
|
+
### Settings
|
|
103
|
+
|
|
104
|
+
The app's only deployment settings select where it draws and which palette it uses:
|
|
105
|
+
|
|
106
|
+
| Field | Default | Meaning |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `screen` | `'alternate'` | `'alternate'` draws the transcript in the alternate screen with the app's own scroll window; `'inline'` draws into the normal screen and leaves history to the terminal's scrollback. |
|
|
109
|
+
| `colorScheme` | `'auto'` | Palette selection. `'auto'` reads the terminal's exported background signal and otherwise stays dark; `'dark'` and `'light'` pin the palette. |
|
|
110
|
+
|
|
111
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tui-app) is the exhaustive source for every accepted field and its JSDoc. `DSH_TUI_SCREEN` and `DSH_TUI_COLOR_SCHEME` in the bundle patch supply the defaults.
|
|
112
|
+
|
|
113
|
+
-----
|
|
114
|
+
|
|
115
|
+
<a id="understand-the-implementation"></a>
|
|
116
|
+
## Understand the implementation
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary>Implementation internals — click to expand</summary>
|
|
120
|
+
|
|
121
|
+
The app owns one `TuiSession` and one terminal UI. It waits for the complete composition (`ctx.get('loader')?.await()`) so the Agent's scoped tools and adapters are mounted, creates or resumes the Agent through the core registry, and then subscribes to the durable `session/event` log, the live `agent/assistant-stream` feed, `agent/status`, and the `subagent/start` / `subagent/end` edges that report delegated work the footer and cancel keys follow. A resumed session's earlier events predate those subscriptions, so the app reads them back from the persistence backend through [`TuiSession.history()`](src/session.ts) — stopping at the Session's first live sequence so an event appended after resume is not folded twice — and folds them into the transcript before accepting input; an event that lands while that read is in flight is buffered and applied after the stored rows, keeping log order.
|
|
122
|
+
|
|
123
|
+
While the renderer owns the terminal, console writes from mounted plugins are captured and appended as ordinary notice rows. A raw write would land on the row under the cursor, and the differential renderer would leave it there until that row's own content changed.
|
|
124
|
+
|
|
125
|
+
### Rendering
|
|
126
|
+
|
|
127
|
+
[`src/transcript.ts`](src/transcript.ts) folds those events into ordered rows — prompts, assistant messages, retained reasoning, Tool calls with their settled outcome, approval decisions, and app notices — and invalidates its rendered lines through a revision counter, while the view re-renders only the rows that revision changed: it caches each row's lines by row identity and viewport width, so a streamed delta renders the live row alone instead of the whole transcript. A prompt renders its image markers before its text, one marker per attached image in content order, because the terminal has no thumbnail and the durable content blocks are the record of what was attached. Message rows mark their role on the first line: a prompt opens with `›` and an assistant message with `◆`, in that role's color, while every later line aligns under a two-space indent. The first-line glyphs keep the roles apart even without color, and no rule is drawn into the continuation lines, so copying a reply does not drag a vertical bar along. Injected context is model input rather than conversation, so it produces a row only when its producer declared a one-line `notice` form: a workspace-instruction, skill-catalog, or runtime-context message stays in the session log, while a model switch, a plan-mode change, or a goal appears as an app notice. The footer sits under the composer in both screen strategies and occupies two lines: the workspace and Agent state right-aligned against the routed model and its reasoning effort, then the occupancy of the next request against the routed model's capacity, with the whole log's token figures at the bottom-right edge. Once the session has delegated any child, the occupancy is followed by a `● subagents (running/total)` marker — `●` while a child activation runs, `○` once every recorded child has settled — read from the same catalogs `/subagents` lists, and shown alone on a frame that knows no occupancy yet. Those figures are the decode throughput in tokens per second, the billed prompt-plus-output total, and the cache-read share of billed prompt tokens; each is omitted until it has data, the rate is the whole-log average the Web stats strip reports rather than a live instantaneous one, and the group yields before the occupancy when the terminal is too narrow for both. Plan mode adds a `plan` marker beside the Agent state, toggled by `Shift+Tab` through `ctx.planMode`. The key hints are the placeholder of the empty composer rather than a footer line: the composer is wrapped so its content line shows them until the first typed character, because the editor component renders no placeholder of its own. Occupancy and capacity come from the `contextPressure` projection of the mounted `ctx.tokenMeter`, the totals from its `tokenUsage` projection, and the throughput from the `sessionStats` projection this bundle inserts; the `(auto)` marker comes from `ctx.compaction.autoCompactionEnabled`; the model's provider qualifies the label only when the deployment registers several. [`src/views.ts`](src/views.ts) turns rows into terminal lines with [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui) components, truncating every line to the viewport width because the renderer treats an over-wide line as a component defect, and flattening a Tool heading to one line because one row owns exactly one terminal row: a multi-line argument would otherwise print its own line breaks and spill the row into the pinned footer. A picker or prompt renders as ordinary output rather than a dialog — a plain title, a blank row, and the body — anchored directly above the composer in the alternate-screen layout, with every row padded to the viewport width so the transcript cannot show through beside it. The alternate-screen renderer owns scrolling: the transcript is its primary scroll view, so PageUp/PageDown and the wheel move the transcript while the status bar and composer stay pinned; stopping the renderer writes the whole transcript back to the normal screen, so the output stays selectable after exit. `stop()` then flushes the session and writes the resume command to stderr, after the restore because the surface no longer owns the screen, and only where a persistence backend is mounted. A Tool's declared Host presentation decides its row: the call and result views resolved through `ctx.tools` while the fold runs render as a `card: 'diff'` unified diff with the Tool's own heading and a `+added -removed` count, a `card: 'terminal'` command with its description, working directory, output, and exit-status pill, a `card: 'read'` numbered line window, a `card: 'search'` grouped match or path list with its cap signal, a `card: 'web'` cited-source or fetch summary, and a `card: 'generic'` title with its salient input. A card body folds after a bounded number of lines; a settled result view replaces the pending one, an omitted result view keeps the call-time card, and a failed mutation falls back to the ordinary name-and-summary row with its error text. A Tool row's ordinary result body renders in the palette's tool-result grey rather than the assistant body's terminal foreground. The palette follows the terminal background: `colorScheme` pins dark or light, and `auto` reads the terminal's `COLORFGBG` background signal, defaulting to dark. Switching sessions releases the transcript view’s cached lines and Markdown components. Provider reasoning stays visible after streaming ends and is restored from the session log when resuming.
|
|
128
|
+
|
|
129
|
+
### Interaction seams
|
|
130
|
+
|
|
131
|
+
The app requires the approval service to record remembered tool grants and answers the two seams the Agent pauses on. `ctx.on('approval/request', …)` offers Allow once / Always allow / Reject for this app's own Agent and every live subagent descendant, naming the child's descriptor label in the heading; every other Agent's request is delegated, so a composition that also mounts another surface keeps its own answerers authoritative. Always allow records a per-tool grant on the app's own root Agent, so the choice covers the session and every live subagent; later asks for that tool settle without a prompt, their audit rows read `allowed always`, and a resumed session keeps the grant. Prompts are serialized, so concurrent requests queue in arrival order, and a skipped or cancelled prompt resolves `cancelled`, which the approval service already treats as fail-closed. Every ask and decision also folds into a durable `approval · <tool> — <outcome>` transcript row: the app's own decisions appear in its transcript, and a child's appear in the child's watch viewer. `ctx.on('user-questions/request', …)` renders the question's options as a picker — a checkable list when the question sets `multiSelect`, where Space toggles the highlighted option and Enter submits the checked labels in list order, doing nothing while none is checked — or a free-text input when the question declares none. Both option kinds gain a final `Other…` row that opens that free-text input instead: a single selection is replaced by the typed answer and a multi-selection keeps its checked labels alongside it. Escape skips the question with an empty selection so the batch advances, and Ctrl+C fails the asking Tool with `ASK_ABORTED`; a plan review answered with Keep planning — or left unanswered any other way — fails it with `ASK_CANCELLED`, which plan mode reads as the user taking the turn back, while its `Other…` feedback reaches the model instead. A question's `detail` renders as markdown above that control, in a viewport sized to the rows the control leaves; a prompt whose question carries a detail takes every row above the pinned composer and footer, while one without a detail keeps the picker's own height cap so a short list never fills a tall terminal. While the detail overflows, Up/Down, PageUp/PageDown, and the wheel scroll it and Left/Right move the picker, so a plan review shows the plan the model submitted rather than only its approval options. The picker itself installs no mouse handler, so a stray click cannot move its highlight or confirm an option, and the wheel pages the transcript behind the prompt; only a detail that overflows takes the wheel for its own viewport. One prompt owns the keyboard at a time. A question's heading wraps across at most three rows — a dim label carrying the batch position and header above the question — with a timed window's countdown pinned to its first row.
|
|
132
|
+
|
|
133
|
+
`ask_user_question` runs in its timed form. The app claims the call's foreground window from the service and counts the seconds left in the heading, and a window the user does not answer inside — including one they cancel — settles the Tool as pending rather than failing it, so the question stays answerable. `/questions` lists every call this session still owes an answer, presents each question through the same prompt a live one uses, and hands the finished batch to the service, which steers it into the Agent as a user message the transcript shows as the user's reply. The transcript row of a settled call lists each question with the labels the user chose, or a skip marker, and a still-waiting timed call says so instead of showing its result JSON.
|
|
134
|
+
|
|
135
|
+
### Clipboard images
|
|
136
|
+
|
|
137
|
+
Ctrl+V reads the system clipboard through the platform's own reader ([`src/clipboard.ts`](src/clipboard.ts)): `osascript`'s JavaScript automation runtime reading `NSPasteboard` directly on macOS, PowerShell on Windows and WSL, `wl-paste` on Wayland, and `xclip` on X11. A reader stages bytes in a temporary file that the read deletes, and its declared media type is only a claim — the attachment service verifies it against the decoded bytes. The footer reports `pasting image…` from the key press until the bytes arrive, because a platform reader runs as a child process and a large clipboard image can take most of a second to stage. The bytes stay in memory under the marker the composer shows. A submission citing a marker resolves the exact routed model's declared input modalities, admits the batch through `ctx.attachments.admitPromptContent(…)`, and hands the Agent one user message carrying the prompt text followed by the admitted image blocks. A refusal — no attachment store, a model that excludes image input, or a failed admission — restores the draft rather than sending a partial prompt.
|
|
138
|
+
|
|
139
|
+
### Patch surface over base
|
|
140
|
+
|
|
141
|
+
The patch rides over `dsh-base` and adds no Host or browser row. It restates the coding persona the other surfaces set, inserts the startup provider and the app, and leaves the base's model-facing rows in the host plane: this surface is single-session, so its Agent composes them process-wide instead of per session. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--resume`, `--provider`, `--model`, and `--watch`, and provides `tuiStartup`; the app row injects that service, so `--help` and a rejected invocation mount no terminal UI at all. `--watch` is mutually exclusive with the session flags and boots [`src/watch/viewer.ts`](src/watch/viewer.ts) instead of the interactive app. The first local subagent starts the in-process Node HTTP listener owned by [`src/watch/server.ts`](src/watch/server.ts); it serves only that TUI's local subagent tree and exits with the TUI. The patch also mounts `tool-ask-user` in its `mode: timed` form, because the base layer provides the question service but no question tool. It sets `tool-fs.applyPatch: true`, adding `apply_patch` beside `edit`; `dsh-base` keeps the tool switched off for the other surfaces. The patch also sets `session-log-deepseek` to `enabled: false`, so this fork keeps Session logs on the machine instead of contributing the upstream `dsh_session_log` suffix to official DeepSeek requests; a deployment that wants that suffix re-enables the row through a `--patch` overlay or the profile's own `cordis.patch.yml`. The patch sets `subagent.delegatedApproval: inherit`, so a local child inherits the parent's effective approval policy and the app's descendant answerer can decide its asks; deployments that need unattended delegation restore the package default `never`.
|
|
142
|
+
|
|
143
|
+
### Source map
|
|
144
|
+
|
|
145
|
+
| File | Role |
|
|
146
|
+
|---|---|
|
|
147
|
+
| [`src/index.ts`](src/index.ts) | The `tui-app` plugin: launcher facts, boot, and failure exit |
|
|
148
|
+
| [`src/startup.ts`](src/startup.ts) | The `tui-startup` provider: flag family and `--help` |
|
|
149
|
+
| [`src/app.ts`](src/app.ts) | Surface construction, event wiring, input routing, teardown |
|
|
150
|
+
| [`src/diagnostics.ts`](src/diagnostics.ts) | Console capture while the surface owns the terminal |
|
|
151
|
+
| [`src/session.ts`](src/session.ts) | Agent create/resume, stored-history read, prompt submission, route switching |
|
|
152
|
+
| [`src/transcript.ts`](src/transcript.ts) | The event fold into renderable rows |
|
|
153
|
+
| [`src/diff.ts`](src/diff.ts) | The pure file-diff row model |
|
|
154
|
+
| [`src/tool-view.ts`](src/tool-view.ts) | The Host tool-presentation bridge |
|
|
155
|
+
| [`src/images.ts`](src/images.ts) | Composer image markers and the submission fold |
|
|
156
|
+
| [`src/clipboard.ts`](src/clipboard.ts) | Platform clipboard image readers and their staging files |
|
|
157
|
+
| [`src/views.ts`](src/views.ts) | Transcript, status bar, and modal panel components |
|
|
158
|
+
| [`src/commands.ts`](src/commands.ts) | Slash-command catalog, dispatch, and model catalog |
|
|
159
|
+
| [`src/interactions.ts`](src/interactions.ts) | Approval and user-question answerers |
|
|
160
|
+
| [`src/ansi.ts`](src/ansi.ts) | Semantic styles and the component themes |
|
|
161
|
+
| [`src/watch/protocol.ts`](src/watch/protocol.ts) | NDJSON wire vocabulary and record validation |
|
|
162
|
+
| [`src/watch/record.ts`](src/watch/record.ts) | Private runtime record publication and cleanup |
|
|
163
|
+
| [`src/watch/server.ts`](src/watch/server.ts) | Loopback stream service, child state, and bounded client queues |
|
|
164
|
+
| [`src/watch/client.ts`](src/watch/client.ts) | Authenticated stream reader and wire validation |
|
|
165
|
+
| [`src/watch/history.ts`](src/watch/history.ts) | Read-handle history access for the viewer |
|
|
166
|
+
| [`src/watch/viewer.ts`](src/watch/viewer.ts) | Read-only watch surface and its footer |
|
|
167
|
+
| [`cordis.patch.yml`](cordis.patch.yml) | The terminal patch over `dsh-base` |
|
|
168
|
+
| — | No runtime invariant companion is published; the app registers no registry, and its watch records are disposable runtime data rather than Session state. |
|
|
169
|
+
| [`tests/tui-app.spec.ts`](tests/tui-app.spec.ts) | Composer routing, clipboard paste, rendering, modal answers, and exit |
|
|
170
|
+
| [`tests/diagnostics.spec.ts`](tests/diagnostics.spec.ts) | Console formatting, plain-text sanitization, and method restoration |
|
|
171
|
+
| [`tests/transcript.spec.ts`](tests/transcript.spec.ts) | The event fold, the footer, the placeholder composer, and line-width bounds |
|
|
172
|
+
| [`tests/clipboard.spec.ts`](tests/clipboard.spec.ts) | Every platform reader path and the staging-file adapters |
|
|
173
|
+
| [`tests/images.spec.ts`](tests/images.spec.ts) | Composer marker parsing |
|
|
174
|
+
| [`tests/startup.spec.ts`](tests/startup.spec.ts) | Command-line parsing over a real Loader tree |
|
|
175
|
+
| [`tests/watch.spec.ts`](tests/watch.spec.ts) | Record publication, authenticated streaming, continuity checks, history fallback, and the viewer |
|
|
176
|
+
|
|
177
|
+
### Invariant ownership
|
|
178
|
+
|
|
179
|
+
No invariant companion is published: the app contributes no registry, its watch records are disposable runtime data with an owner-only access token, and its observable contract is the terminal surface, which the surface tests drive through a substituted Terminal.
|
|
180
|
+
|
|
181
|
+
</details>
|
|
182
|
+
|
|
183
|
+
-----
|
|
184
|
+
|
|
185
|
+
<a id="further-exploration"></a>
|
|
186
|
+
## Further Exploration
|
|
187
|
+
|
|
188
|
+
Read these pages to go deeper into the shared core, the sibling surfaces, or the terminal library.
|
|
189
|
+
|
|
190
|
+
- [Bundle package map](../README.md) — the surfaces built on the same core.
|
|
191
|
+
- [dsh-base](../base/README.md) — the shared core this surface runs on.
|
|
192
|
+
- [dsh-headless](../headless/README.md) — the one-shot sibling for scripts and CI.
|
|
193
|
+
- [dsh-web-app](../web-app/README.md) — the browser sibling for multi-turn work.
|
|
194
|
+
- [dsh-cmdline](../../boot/cmdline/README.md) — how the launcher hands the command line to the app.
|
|
195
|
+
- [Interactive delegated approvals](../../../.agents/notes/implemented/architecture/2026-10-22-interactive-delegated-approvals.md) — why this surface answers its live descendants and how the policy is seeded.
|
|
196
|
+
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tui-app) — every accepted config field and its source declaration.
|
|
197
|
+
|
|
198
|
+
-----
|
|
199
|
+
|
|
200
|
+
<a id="model-experience"></a>
|
|
201
|
+
## Model Experience
|
|
202
|
+
|
|
203
|
+
### Interactive coding session
|
|
204
|
+
|
|
205
|
+
#### What the model sees
|
|
206
|
+
|
|
207
|
+
The composition enables `apply_patch`, so every request carries that tool's schema and its system-prompt guidance beside the base file tools. The app submits the typed prompt as an ordinary user message and renders the model's own output back; `/help`, `/quit`, `/new`, `/sessions`, `/resume`, and `/model` never reach the model. A model switch appends the shared model-selection notice, exactly as it does on the other surfaces. Shift+Tab enters or leaves plan mode through the same service `/plan` drives, so the model sees the plan-policy section and the user-switch notice a typed command produces. A clipboard paste adds the admitted image to that same message as a durable image reference, exactly as an upload from another surface does; the composer's `[Image N]` markers are composer text and never reach the model.
|
|
208
|
+
|
|
209
|
+
#### Token effect
|
|
210
|
+
|
|
211
|
+
Prompts and responses carry their ordinary token cost. The `apply_patch` schema and guidance are input tokens on every request. Nothing the terminal renders — the footer, the Tool row folding, or the scroll window — adds a request or a token; the occupancy, throughput, and token-total figures are read from local measurement projections, not from an extra provider call. Toggling plan mode adds no request of its own and changes only the next request's plan-policy section. An attached image is counted at the routed model's declared image pricing when it declares one, so the footer reflects the request the model will actually receive.
|
|
212
|
+
|
|
213
|
+
#### KV Cache effect
|
|
214
|
+
|
|
215
|
+
The profile's extra `apply_patch` schema and guidance are part of the stable request prefix and do not change between turns; the app otherwise adds nothing to the prefix and only routes the typed prompt through the composed tree.
|
|
216
|
+
|
|
217
|
+
## Known Limitations and Deferred Work
|
|
218
|
+
|
|
219
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
These limits define what the terminal surface does not do. They are current constraints of this bundle, not a backlog of the browser surface.
|
|
223
|
+
|
|
224
|
+
- **One Agent per invocation** — the app owns a single session; switching to another Agent means `/new` or `/resume`, which opens the replacement before closing the current one so a failed open leaves the live session in place.
|
|
225
|
+
- **Always allow is app-wide and session-scoped** — choosing it on any prompt, including a child's, records the grant on the root Agent and covers every live subagent for the rest of the session; the surface has no revoke control.
|
|
226
|
+
- **Subagent approvals need a live lineage** — the app answers only its own Agent and live descendants; a child whose parent chain is no longer live, or whose profile pins `delegatedApproval: never`, resolves without a terminal prompt.
|
|
227
|
+
- **Keep planning waits for a message** — the terminal review has no free-text feedback field, so choosing Keep planning closes the review and hands the turn back, keeping plan mode active; the adjustment is the user's next prompt rather than an answer carried on the review.
|
|
228
|
+
- **A prompt's height is fixed when it opens** — the panel sizes itself from the terminal once, so resizing the terminal during a review does not resize it; close and reopen the prompt to pick up the new height.
|
|
229
|
+
- **Images come only from the clipboard** — the composer attaches PNG, JPEG, WebP, and GIF bytes read from the system clipboard, through `osascript` reading `NSPasteboard` on macOS, PowerShell on Windows and WSL, `wl-paste` on Wayland, or `xclip` on X11. A host with none of those readers reports the paste as an empty clipboard, and no file picker, drag-and-drop, or non-image attachment exists.
|
|
230
|
+
- **A late reply is a follow-up** — the timed Tool already returned its pending result, so `/questions` steers the answer into the Agent as a new user message; the recorded call is not resumed.
|
|
231
|
+
- **Tool output is folded** — a Tool result shows its first lines plus a count of the remainder; the complete output stays in the session log, not on screen.
|
|
232
|
+
- **Cards render as text** — every Host card draws as terminal rows: a read window carries no syntax highlighting, a generic card's `kind` icon and `locations` follow-along are not drawn, and a terminal card replays the captured output rather than a live session.
|
|
233
|
+
- **Occupancy is an estimate** — the footer's percentage anchors on the last provider-reported prompt size and heuristically reprices what the surface gained or lost since; it is a reference for the user, not a billing or admission input.
|
|
234
|
+
- **Launcher-owned exit** — like every surface, the app starts only through the `dsh` profile, because only the launcher provides the bounded exit request.
|
|
235
|
+
- **No recorded-session snapshot** — the keyless snapshot harness drives shipped profiles over stdio, while this surface owns a terminal; its acceptance is the package tests plus a pseudo-terminal run rather than a snapshot fixture, so a regression in terminal layout needs the surface tests to be extended rather than a re-recorded snapshot.
|
|
236
|
+
- **Watching is local and first-version** — only local subagents started by a TUI, including nested ones, are viewable. Web, headless, SDK, and remote-machine sessions are out of scope, the viewer never reconnects automatically, and the writer side does not relay raw tool stdout. Tokens generated before the original process crashes but not committed to the Session log are not recoverable; the viewer keeps only what it already displayed and reports the interruption.
|
|
237
|
+
|
|
238
|
+
<a id="dev-note"></a>
|
|
239
|
+
### Dev Note
|
|
240
|
+
|
|
241
|
+
<details>
|
|
242
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
243
|
+
|
|
244
|
+
None.
|
|
245
|
+
|
|
246
|
+
</details>
|