@songtonyli/dsh-cli 0.1.13 → 0.1.14
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.md +1 -1
- package/bin/dsh.mjs +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-cordis-client-runner/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-tui-app/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-tui-app/README.md +8 -8
- package/node_modules/@deepseek-ai/dsh-tui-app/README.zh.md +8 -8
- package/node_modules/@deepseek-ai/dsh-tui-app/lib/index.js +633 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ workspace and no build step. Native packages that ship one build per platform
|
|
|
26
26
|
ripgrep, the PTY backend's addons, sharp, koffi, and the Landlock launcher — are
|
|
27
27
|
ordinary optional dependencies, so npm resolves the right one for your machine.
|
|
28
28
|
|
|
29
|
-
Requires Node `^22.19.0 || >=24.0.0`. Version 0.1.
|
|
29
|
+
Requires Node `^22.19.0 || >=24.0.0`. Version 0.1.14; `dsh --version`
|
|
30
30
|
also reports the harness build the package was assembled from.
|
|
31
31
|
|
|
32
32
|
## License
|
package/bin/dsh.mjs
CHANGED
|
@@ -18,7 +18,7 @@ const argv = process.argv.slice(2)
|
|
|
18
18
|
// version this package publishes. Reporting only one of the two would make an
|
|
19
19
|
// ordinary `npm install @songtonyli/dsh-cli@x` look like it installed something else.
|
|
20
20
|
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) {
|
|
21
|
-
console.log('@songtonyli/dsh-cli 0.1.
|
|
21
|
+
console.log('@songtonyli/dsh-cli 0.1.14 (deepseek-harness 0.1.6-alpha.2)')
|
|
22
22
|
process.exit(0)
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -160,7 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
160
160
|
const SCROLLBAR_LINGER_MS = 2e3;
|
|
161
161
|
/** Format complete-build metadata for the local brand badge. */
|
|
162
162
|
function localBuildVersion() {
|
|
163
|
-
return `0.1.6-alpha.2-
|
|
163
|
+
return `0.1.6-alpha.2-1d90b61-dirty`;
|
|
164
164
|
}
|
|
165
165
|
/** Each panel row subscribes only to its own selection state. */
|
|
166
166
|
function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }) {
|
|
@@ -3319,7 +3319,7 @@ call: (method, args = null) => env.invoke(method, args) }, harnessTrap(), ...Obj
|
|
|
3319
3319
|
occupants: ["client-ui-plan PlanReviewOpen"],
|
|
3320
3320
|
replaceRisk: "none",
|
|
3321
3321
|
example: "return {\n inject: ['slots'],\n apply(ctx) {\n ctx.slots.inject('conversation.plan-review.actions', () => ctx.slots.register(\n { name: 'conversation.plan-review.actions', id: 'my-entry', order: 100, label: 'My entry' },\n () => React.createElement('div', null, 'hello'),\n ))\n },\n}",
|
|
3322
|
-
source: "packages/client/ui-user-questions/src/client/contract/slots.ts:
|
|
3322
|
+
source: "packages/client/ui-user-questions/src/client/contract/slots.ts:22"
|
|
3323
3323
|
},
|
|
3324
3324
|
{
|
|
3325
3325
|
key: "conversation.session",
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/bundle/tui-app/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 609455db6daa218a2d71ed685495d1e9a5ca7283
|
|
6
|
+
README.zh.md: 4b943b769e10c62bfe960fb133f311d759f63093
|
|
@@ -40,11 +40,11 @@ On quit the app prints `dsh: session <id> saved; resume with: dsh --profile tui
|
|
|
40
40
|
|
|
41
41
|
### The screen
|
|
42
42
|
|
|
43
|
-
The header names the session by its title once one is generated or set, with the id beside it. The transcript grows in the terminal's own scrollback: your prompts start with `›` (attachments listed under them), assistant reasoning is dim above the Markdown reply, and each tool call is a card with a status glyph, the tool name, the presenter headline, and a body folded to `toolPreviewLines` rows. Streamed text fades in and every word keeps its own clock: a word enters near the terminal's background color and brightens to the color it settles in over `streamFadeSteps`
|
|
43
|
+
The header names the session by its title once one is generated or set, with the id beside it. The transcript grows in the terminal's own scrollback: your prompts start with `›` (attachments listed under them), assistant reasoning is dim above the Markdown reply, and each tool call is a card with a status glyph, the tool name, the presenter headline, and a body folded to `toolPreviewLines` rows. Streamed reply text fades in and every word keeps its own clock: a word enters near the terminal's background color and brightens to the color it settles in over `streamFadeSteps` ticks of `streamFadeStepMs` each, so a fast stream leaves a longer trail of brightening words rather than a darker one. Streamed reasoning and a tool card float out over the same duration: they appear at a lifted color and recede to the dim italic or palette colors they settle in; a card redrawn from persisted history carries no fade, and text that has settled is never dimmed again. Below the transcript sit a spinner while the agent works, any open prompt, the editor, the subagent panel, and the footer: a status bar that, unfocused, is one line of key facts. The editor's caret is the terminal's own blinking bar: the app asks for that shape at start, gives your default back on quit, and draws no caret at all while the status bar or the panel holds the keyboard. The model, effort (`effort default` when the selection leaves reasoning to the model), and workspace path (shortened with `~` and `…/` when long) are always present; every other segment appears only when its fact exists — the permission preset, the running turn's elapsed time, cumulative token usage, the context window percentage, todo and goal and plan-mode markers from the projection seam, and the count of pending attachments — and `Shift+Down` expands the bar around the selected segment so that segment stays on screen. Compaction and model-request retries appear as notices, the same facts the browser's markers carry.
|
|
44
44
|
|
|
45
45
|
While the keyboard walks the conversation an inspector is docked directly above the editor. It names the focused section — its position among the navigable blocks, the turn it belongs to, and what the section is, such as `3/12 · turn 2 · bash git status · result` — draws the block's parts as a strip like `‹ reasoning · reply ›` when it has more than one, shows the section's own source rows cut to `focusPreviewLines` with `… <n> more rows · Enter opens the page` under them, and ends with a dim line of the keys it answers. The focused block is also marked where it stands, with a two-column gutter that is dim beside the block's other lines and accented beside the focused section's own, and its content wraps two columns narrower while the mark is drawn. That mark reaches only the lines pi-tui still repaints differentially — the last `rows` lines of the last frame it wrote, a boundary a taller frame raises and a shorter one never lowers — because changing anything above them would clear the terminal's scrollback; a block that has scrolled past that point keeps no gutter and its inspector heading reads `off screen` instead, and a fade whose rows reach that edge settles them to their final colors in the frame that writes them, so nothing stays dim in the scrollback.
|
|
46
46
|
|
|
47
|
-
The subagent panel is drawn while a subagent session under the bound one is resident, or the listing carries a candidate it could not read.
|
|
47
|
+
The subagent panel is drawn while a subagent session under the bound one is resident, or the listing carries a candidate it could not read. Unfocused it is one summary line with the listed count and the first child's key label; focused, its heading counts what it lists, and each row gives the child's depth indent, its label or id, its mode (`one-shot` or `continuable`), `resident`, whether its agent is `running` or `idle`, its elapsed time — the open turn's, else the total its settled turns took — and its token usage, as far as this process's own view of that child and the composed projections carry them. Six rows are drawn at most, with `+<n> more · /subagents lists them all` under them; a candidate the listing could not interpret draws as `unreadable: <reason>` and opens nothing, and a failed listing keeps the rows the last good one produced with `listing failed: <reason>` beneath. The panel disappears with its last row.
|
|
48
48
|
|
|
49
49
|
### Keys and commands
|
|
50
50
|
|
|
@@ -64,7 +64,7 @@ The subagent panel is drawn while a subagent session under the bound one is resi
|
|
|
64
64
|
|
|
65
65
|
The conversation, the subagent panel, and the status bar stack in that order, and `Up` / `Down` walk the whole stack without wrapping at either end. While the conversation has focus, `Up` / `Down` move between blocks — your prompts, assistant messages, and tool cards, with notices and printed reports skipped — `Left` / `Right` move between a block's parts, which are the reasoning and the reply of a message and the call and the result of a tool, `Enter` opens the focused part as a read-only page carrying its full rows and comes back to the same part, and `Esc` returns focus to the editor. The rows a part carries are the block's own source text, so a reply reads as the Markdown the model wrote rather than the rendering drawn above. Every other key is consumed there, apart from `Ctrl+C` and `Ctrl+D`; a session switch drops the focus back to the editor, and a session with nothing to inspect yet answers `Shift+Up` with `nothing in the transcript to inspect yet` and leaves the keyboard in the editor.
|
|
66
66
|
|
|
67
|
-
While the status bar has focus, `Left` / `Right` and `Tab` / `Shift+Tab` move between segments and wrap at both ends, `Up` leaves the bar for the subagent panel's last row while it is drawn and for the conversation otherwise, `Enter` opens the selected segment while the bar keeps focus, and `Esc` returns focus to the editor. No other key reaches the editor while the bar has focus; `Ctrl+C` and `Ctrl+D` keep their usual meaning and return focus to the editor.
|
|
67
|
+
While the status bar has focus, `Left` / `Right`, `Shift+Left` / `Shift+Right`, and `Tab` / `Shift+Tab` move between segments and wrap at both ends, `Up` leaves the bar for the subagent panel's last row while it is drawn and for the conversation otherwise, `Enter` opens the selected segment while the bar keeps focus, and `Esc` returns focus to the editor. No other key reaches the editor while the bar has focus; `Ctrl+C` and `Ctrl+D` keep their usual meaning and return focus to the editor.
|
|
68
68
|
|
|
69
69
|
While the subagent panel has focus, `Up` / `Down` move the selection and continue into the neighboring regions at its ends — `Up` on the first row reaches the conversation and `Down` on the last row the status bar — `Enter` opens that child's session details as a read-only page and comes back to the panel on the same row, and `Esc` returns focus to the editor. Every other key is consumed there as well, apart from `Ctrl+C` and `Ctrl+D`; the panel also hands the keyboard back to the editor when its last row leaves.
|
|
70
70
|
|
|
@@ -124,8 +124,8 @@ An approval request draws `Allow <tool>?` with the asker's reason, the logged ca
|
|
|
124
124
|
| `toolPreviewLines` | `8` | Collapsed tool-card body rows before `Ctrl+O` expands them |
|
|
125
125
|
| `focusPreviewLines` | `12` | Rows of the focused section the docked inspector shows before `Enter` opens the whole of it |
|
|
126
126
|
| `liveRefreshMs` | `1000` | Period of the redraw that advances the `turn` segment and the panel's elapsed values and re-reads a stale subagent listing |
|
|
127
|
-
| `streamFadeSteps` | `8` |
|
|
128
|
-
| `streamFadeStepMs` | `33` |
|
|
127
|
+
| `streamFadeSteps` | `8` | How many ticks a fade lasts: reply text fades in, reasoning and tool cards float out, over `streamFadeSteps × streamFadeStepMs` |
|
|
128
|
+
| `streamFadeStepMs` | `33` | One fade tick, and the repaint period while anything is still fading; duration is `streamFadeSteps × streamFadeStepMs` |
|
|
129
129
|
| `reducedMotion` | `false` | Draw streamed text, streamed reasoning, and tool cards in their settled colors, with no fade and no repeating repaint |
|
|
130
130
|
| `openBrowser` | `true` | Hand marked authorization pages to the local default browser |
|
|
131
131
|
|
|
@@ -167,7 +167,7 @@ The patch rides over `dsh-base`: it sets the coding persona prefix and cwd suffi
|
|
|
167
167
|
| [`src/navigation.ts`](src/navigation.ts) | The transcript as sections, the cursor that walks them, and the inspector heading |
|
|
168
168
|
| [`src/inspector.ts`](src/inspector.ts) | The docked inspector: the focused section's heading, parts strip, folded rows, and its mounted component |
|
|
169
169
|
| [`src/screen.ts`](src/screen.ts) | The main screen with the settle passes between building a frame and writing it, the repaint window each pass is judged against, and the per-block repaint floor |
|
|
170
|
-
| [`src/fade.ts`](src/fade.ts) | The streamed-text fade: the wall-clock tail tracker, the block-fade clock and registry, the
|
|
170
|
+
| [`src/fade.ts`](src/fade.ts) | The streamed-text fade: the wall-clock tail tracker, the block-fade clock and registry, the fade-in ramp, the float-out mix, and the recolor of rendered lines |
|
|
171
171
|
| [`src/prompts.ts`](src/prompts.ts) | Approval, question, picker, and read-only detail prompts plus the modal queue |
|
|
172
172
|
| [`src/transcript.ts`](src/transcript.ts) | Pure text folding of presentation views, usage, and turn-end reasons |
|
|
173
173
|
| [`src/diff.ts`](src/diff.ts) | Line diff and hunk selection for diff cards |
|
|
@@ -175,8 +175,8 @@ The patch rides over `dsh-base`: it sets the coding persona prefix and cwd suffi
|
|
|
175
175
|
| [`src/completion.ts`](src/completion.ts) | Slash-command and `@`-reference completion for the editor |
|
|
176
176
|
| [`src/editor.ts`](src/editor.ts) | The prompt editor without pi-tui's drawn block cursor, and the DECSCUSR sequences for the terminal's own caret |
|
|
177
177
|
| [`src/status.ts`](src/status.ts) | Projection-seam facts and the sections the `/status` report and the segment details share; compaction and retry notices |
|
|
178
|
-
| [`src/footer.ts`](src/footer.ts) | The status bar: the ordered segments, each segment's detail rows, and the
|
|
179
|
-
| [`src/subagent-panel.ts`](src/subagent-panel.ts) | The live subagent panel: one descendant listing plus sampled live facts become its rows, and the
|
|
178
|
+
| [`src/footer.ts`](src/footer.ts) | The status bar: the ordered segments, each segment's detail rows, the one-line unfocused facts, and the focused sliding window |
|
|
179
|
+
| [`src/subagent-panel.ts`](src/subagent-panel.ts) | The live subagent panel: one descendant listing plus sampled live facts become its rows, the unfocused summary line, and the focused listing |
|
|
180
180
|
| [`src/catalog.ts`](src/catalog.ts) | Rows for `/settings`, `/plugins`, `/subagents`, `/deliverables`, `/changes`, and `/outline`, and the `/plugins` management verbs |
|
|
181
181
|
| [`src/todos.ts`](src/todos.ts) | The todo list: the status glyphs, the picker rows, and one item's detail rows |
|
|
182
182
|
| [`cordis.patch.yml`](cordis.patch.yml) | The terminal patch over `dsh-base` |
|
|
@@ -40,11 +40,11 @@ dsh tui --no-open # print sign-in URLs without opening a
|
|
|
40
40
|
|
|
41
41
|
### 屏幕布局
|
|
42
42
|
|
|
43
|
-
页眉在标题生成或设置后以标题命名会话,并在旁边显示 id。对话记录在终端自身的回滚区中增长:你的提示以 `›` 开头(附件列在其下),assistant 的推理以暗色显示在 Markdown 回复上方,每次工具调用是一张卡片,含状态符号、工具名、呈现器标题,以及折叠到 `toolPreviewLines`
|
|
43
|
+
页眉在标题生成或设置后以标题命名会话,并在旁边显示 id。对话记录在终端自身的回滚区中增长:你的提示以 `›` 开头(附件列在其下),assistant 的推理以暗色显示在 Markdown 回复上方,每次工具调用是一张卡片,含状态符号、工具名、呈现器标题,以及折叠到 `toolPreviewLines` 行的正文。流式回复文字会淡入,而且每个词各走各的时钟:一个词以接近终端背景色的亮度出现,并经 `streamFadeSteps` 次、每次 `streamFadeStepMs` 提亮到它最终稳定的颜色,因此流得更快只会留下更长的一串正在提亮的词,而不会更暗。流式推理与工具卡片在同一时长内浮出:它们以抬高的颜色出现,再退回到稳定时的暗色斜体或调色板颜色;从持久化历史重绘的卡片不带淡入,已经稳定下来的文字也不会再被调暗。对话记录下方依次是 agent 工作时的旋转指示、任何打开的提示、编辑器、子 agent 面板,以及页脚:未聚焦时是一行关键事实的状态栏。编辑器的光标是终端自身的闪烁竖条:应用在启动时请求这一形状,退出时把你的默认形状还回去,而状态栏或面板持有键盘期间完全不绘制光标。模型、推理强度(选择把推理交给模型时标为 `effort default`)以及 workspace 路径(过长时以 `~` 与 `…/` 缩短)始终出现;其余分段只在其事实存在时出现——权限预设、正在进行轮次的已用时间、累计 token 用量、上下文窗口百分比、来自投影接缝的 todo、目标与计划模式标记、待发送附件数量——`Shift+Down` 展开状态栏并使所选分段留在屏幕上。压缩与模型请求重试以通知形式出现,与浏览器标记承载的事实相同。
|
|
44
44
|
|
|
45
45
|
键盘在对话记录中移动时,编辑器正上方停靠着一个检视面板。它写出当前聚焦小节的名称——它在可导航块中的位置、所属轮次,以及这是哪一种小节,例如 `3/12 · turn 2 · bash git status · result`——在块含多个部分时把这些部分绘制成形如 `‹ reasoning · reply ›` 的一条选择行,把该小节自身的源文本行截到 `focusPreviewLines` 行并在其下写出 `… <n> more rows · Enter opens the page`,最后以一行暗色文字列出它所应答的按键。被聚焦的块也会在原地被标记:一条两列宽的标记条,在块的其他行旁为暗色、在被聚焦小节自身的行旁为强调色,标记绘制期间该块的内容按窄两列换行。这个标记只能落在 pi-tui 仍以差分方式重绘的行上——也就是它写出的上一帧的最后 `rows` 行,这条边界只会被更高的帧抬高、不会被更矮的帧降低——因为改动其上方的任何一行都会清空终端的回滚区;已经滚过该位置的块不带标记条,其检视面板标题改为写出 `off screen`;淡入中的各行抵达这一边界时,会在写出该帧的同一帧内被结算为最终颜色,因此回滚区里不会留下暗色文字。
|
|
46
46
|
|
|
47
|
-
子 agent 面板在绑定会话之下有常驻的子 agent
|
|
47
|
+
子 agent 面板在绑定会话之下有常驻的子 agent 会话、或列表中带有无法读取的候选者时绘制。未聚焦时它是一行摘要,含列出的数量与第一个子会话的关键标签;聚焦时其标题统计所列出的条目,每一行给出该子会话的层级缩进、其标签或 id、其模式(`one-shot` 或 `continuable`)、`resident`、其 agent 处于 `running` 还是 `idle`、其已用时间——进行中轮次的用时,否则是已结束轮次的合计——以及其 token 用量,以本进程对该子会话的可见程度与已组合的投影所能提供的为限。最多绘制六行,其下是 `+<n> more · /subagents lists them all`;列表无法解读的候选者绘制为 `unreadable: <reason>` 且打不开任何页面,读取失败的列表则保留上一次成功读取产生的各行,并在其下写出 `listing failed: <reason>`。面板随其最后一行一同消失。
|
|
48
48
|
|
|
49
49
|
### 按键与命令
|
|
50
50
|
|
|
@@ -64,7 +64,7 @@ dsh tui --no-open # print sign-in URLs without opening a
|
|
|
64
64
|
|
|
65
65
|
对话记录、子 agent 面板与状态栏按此顺序自上而下排布,`Up` / `Down` 走遍整个序列且在两端都不环绕。对话记录持有焦点时,`Up` / `Down` 在块之间移动——你的提示、assistant 消息与工具卡片,通知与打印出的报告会被跳过——`Left` / `Right` 在块的各部分之间移动,也就是一条消息的推理与回复、一次工具调用的调用与结果,`Enter` 把当前聚焦的部分作为只读页面打开、其中带有该部分的完整各行,离开后回到同一部分,`Esc` 把焦点交还编辑器。各部分承载的是块自身的源文本,因此一条回复读到的是模型写下的 Markdown,而不是其上方绘制出的渲染结果。除 `Ctrl+C` 与 `Ctrl+D` 外,其他按键都在此被消费;切换会话会把焦点交回编辑器,尚无可查看内容的会话对 `Shift+Up` 回以 `nothing in the transcript to inspect yet` 并把键盘留在编辑器。
|
|
66
66
|
|
|
67
|
-
状态栏持有焦点时,`Left` / `Right` 与 `Tab` / `Shift+Tab` 在分段之间移动并在两端环绕,`Up` 在面板已绘制时离开状态栏前往面板的最后一行、否则前往对话记录,`Enter` 打开所选分段且状态栏保持焦点,`Esc` 把焦点交还编辑器。状态栏持有焦点期间其他按键不会到达编辑器;`Ctrl+C` 与 `Ctrl+D` 保持其一贯含义,并把焦点交还编辑器。
|
|
67
|
+
状态栏持有焦点时,`Left` / `Right`、`Shift+Left` / `Shift+Right` 与 `Tab` / `Shift+Tab` 在分段之间移动并在两端环绕,`Up` 在面板已绘制时离开状态栏前往面板的最后一行、否则前往对话记录,`Enter` 打开所选分段且状态栏保持焦点,`Esc` 把焦点交还编辑器。状态栏持有焦点期间其他按键不会到达编辑器;`Ctrl+C` 与 `Ctrl+D` 保持其一贯含义,并把焦点交还编辑器。
|
|
68
68
|
|
|
69
69
|
子 agent 面板持有焦点时,`Up` / `Down` 移动选择,并在其两端继续进入相邻区域——在第一行按 `Up` 到达对话记录,在最后一行按 `Down` 到达状态栏——`Enter` 把该子会话的详情作为只读页面打开、离开后回到面板的同一行,`Esc` 把焦点交还编辑器。除 `Ctrl+C` 与 `Ctrl+D` 外,其他按键同样在此被消费;面板的最后一行离开时,它也会把键盘交还编辑器。
|
|
70
70
|
|
|
@@ -124,8 +124,8 @@ dsh tui --no-open # print sign-in URLs without opening a
|
|
|
124
124
|
| `toolPreviewLines` | `8` | `Ctrl+O` 展开前折叠的工具卡片正文行数 |
|
|
125
125
|
| `focusPreviewLines` | `12` | `Enter` 完整打开之前,停靠的检视面板为聚焦小节显示的行数 |
|
|
126
126
|
| `liveRefreshMs` | `1000` | 重绘周期:推进 `turn` 分段与面板中的已用时间,并重新读取已过期的子 agent 列表 |
|
|
127
|
-
| `streamFadeSteps` | `8` |
|
|
128
|
-
| `streamFadeStepMs` | `33` |
|
|
127
|
+
| `streamFadeSteps` | `8` | 一次淡入或浮出持续多少拍:回复文字淡入,推理与工具卡片浮出,时长为 `streamFadeSteps × streamFadeStepMs` |
|
|
128
|
+
| `streamFadeStepMs` | `33` | 一拍淡入或浮出的时长,也是仍在变化时的重绘周期;总时长为 `streamFadeSteps × streamFadeStepMs` |
|
|
129
129
|
| `reducedMotion` | `false` | 以稳定颜色绘制流式文本、流式推理与工具卡片,不做淡入,也不重复重绘 |
|
|
130
130
|
| `openBrowser` | `true` | 把被标记的授权页面交给本地默认浏览器 |
|
|
131
131
|
|
|
@@ -167,7 +167,7 @@ runner 等待完整应用就绪(`ctx.get('loader')?.await()`),并在核心
|
|
|
167
167
|
| [`src/navigation.ts`](src/navigation.ts) | 把对话记录看作各个小节、走遍它们的光标,以及检视面板的标题 |
|
|
168
168
|
| [`src/inspector.ts`](src/inspector.ts) | 停靠的检视面板:聚焦小节的标题、各部分选择行、折叠后的各行,及其挂载的组件 |
|
|
169
169
|
| [`src/screen.ts`](src/screen.ts) | 在构建一帧与写出该帧之间带若干次结算的主屏幕、每次结算所依据的重绘窗口,以及每个块的重绘下界 |
|
|
170
|
-
| [`src/fade.ts`](src/fade.ts) |
|
|
170
|
+
| [`src/fade.ts`](src/fade.ts) | 流式文本淡入与浮出:基于挂钟的尾部追踪器、块时钟与注册表、淡入亮度级别、浮出混合,以及对已渲染行的重新着色 |
|
|
171
171
|
| [`src/prompts.ts`](src/prompts.ts) | 审批、提问、选择器与只读详情提示以及模态队列 |
|
|
172
172
|
| [`src/transcript.ts`](src/transcript.ts) | 呈现视图、用量与轮次结束原因的纯文本折叠 |
|
|
173
173
|
| [`src/diff.ts`](src/diff.ts) | diff 卡片的行 diff 与 hunk 选择 |
|
|
@@ -175,8 +175,8 @@ runner 等待完整应用就绪(`ctx.get('loader')?.await()`),并在核心
|
|
|
175
175
|
| [`src/completion.ts`](src/completion.ts) | 编辑器的斜杠命令与 `@` 引用补全 |
|
|
176
176
|
| [`src/editor.ts`](src/editor.ts) | 去掉 pi-tui 自绘块状光标的提示编辑器,以及终端自身光标所用的 DECSCUSR 序列 |
|
|
177
177
|
| [`src/status.ts`](src/status.ts) | 投影接缝的事实,以及 `/status` 报告与分段详情共享的小节;压缩与重试通知 |
|
|
178
|
-
| [`src/footer.ts`](src/footer.ts) |
|
|
179
|
-
| [`src/subagent-panel.ts`](src/subagent-panel.ts) | 实时子 agent
|
|
178
|
+
| [`src/footer.ts`](src/footer.ts) | 状态栏:有序的各分段、每个分段的详情行、未聚焦时的一行关键事实,以及聚焦时的滑动窗口 |
|
|
179
|
+
| [`src/subagent-panel.ts`](src/subagent-panel.ts) | 实时子 agent 面板:一次后代列表加上采样到的实时事实构成其各行、未聚焦时的摘要行,以及聚焦时的列表 |
|
|
180
180
|
| [`src/catalog.ts`](src/catalog.ts) | `/settings`、`/plugins`、`/subagents`、`/deliverables`、`/changes` 与 `/outline` 的行,以及 `/plugins` 的管理动作 |
|
|
181
181
|
| [`src/todos.ts`](src/todos.ts) | todo 列表:状态符号、选择器行与单个条目的详情行 |
|
|
182
182
|
| [`cordis.patch.yml`](cordis.patch.yml) | 基于 `dsh-base` 的终端 patch |
|
|
@@ -1014,6 +1014,10 @@ const LUMINANCE = {
|
|
|
1014
1014
|
};
|
|
1015
1015
|
/** Largest value one color channel encodes. */
|
|
1016
1016
|
const CHANNEL_MAX = 255;
|
|
1017
|
+
/** Channel scale SGR faint applies when a float-out reads a dim run. */
|
|
1018
|
+
const DIM_CHANNEL = .5;
|
|
1019
|
+
/** How far a float-out lifts a color that is already brighter than `fg` toward white. */
|
|
1020
|
+
const LIFT_TOWARD_WHITE = .5;
|
|
1017
1021
|
/** Encoded sRGB value below which the transfer function is the linear segment. */
|
|
1018
1022
|
const SRGB_LINEAR_CUT = .04045;
|
|
1019
1023
|
/** Linear intensity below which the sRGB transfer function is the linear segment. */
|
|
@@ -1093,6 +1097,34 @@ function mix(from, to, ratio) {
|
|
|
1093
1097
|
return toSrgb(linear + (toLinear(to) - linear) * ratio);
|
|
1094
1098
|
}
|
|
1095
1099
|
/**
|
|
1100
|
+
* Mix `from` toward `to` with the same smoothstep-in-linear-light interpolation
|
|
1101
|
+
* {@link buildFadeRamp} uses. Positions at or below 0 and at or above 1 return
|
|
1102
|
+
* that endpoint's bytes, so a float-out's last computed mix can match the
|
|
1103
|
+
* settled color it recedes toward.
|
|
1104
|
+
* @param from - the color at t = 0.
|
|
1105
|
+
* @param to - the color at t = 1.
|
|
1106
|
+
* @param t - position along the mix; values outside 0..1 clamp to an endpoint.
|
|
1107
|
+
* @returns the mixed sRGB color.
|
|
1108
|
+
*/
|
|
1109
|
+
function mixFadeColor(from, to, t) {
|
|
1110
|
+
if (t <= 0) return {
|
|
1111
|
+
r: from.r,
|
|
1112
|
+
g: from.g,
|
|
1113
|
+
b: from.b
|
|
1114
|
+
};
|
|
1115
|
+
if (t >= 1) return {
|
|
1116
|
+
r: to.r,
|
|
1117
|
+
g: to.g,
|
|
1118
|
+
b: to.b
|
|
1119
|
+
};
|
|
1120
|
+
const eased = t * t * (3 - 2 * t);
|
|
1121
|
+
return {
|
|
1122
|
+
r: mix(from.r, to.r, eased),
|
|
1123
|
+
g: mix(from.g, to.g, eased),
|
|
1124
|
+
b: mix(from.b, to.b, eased)
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1096
1128
|
* Decode one sRGB channel to linear light, by the sRGB transfer function.
|
|
1097
1129
|
* @param value - the channel byte.
|
|
1098
1130
|
* @returns the linear intensity in 0..1.
|
|
@@ -1121,7 +1153,7 @@ function toSrgb(value) {
|
|
|
1121
1153
|
function fadeSgr(style, age) {
|
|
1122
1154
|
if (style.capability === "none") return "";
|
|
1123
1155
|
if (style.capability === "dim") return age < DIM_AGES ? DIM : "";
|
|
1124
|
-
const level = style.ramp[Math.max(0, Math.min(age, style.ramp.length - 1))];
|
|
1156
|
+
const level = style.ramp[Math.max(0, Math.min(Math.floor(age), style.ramp.length - 1))];
|
|
1125
1157
|
if (level === void 0) return "";
|
|
1126
1158
|
return style.capability === "truecolor" ? truecolorSgr(level) : grayscaleSgr(level);
|
|
1127
1159
|
}
|
|
@@ -1163,31 +1195,340 @@ function restoreFor(capability) {
|
|
|
1163
1195
|
/** Every SGR sequence, the sequences a block's own palette styling writes. */
|
|
1164
1196
|
const SGR_SEQUENCE = /\u001b\[[0-9;]*m/g;
|
|
1165
1197
|
/**
|
|
1166
|
-
* Draw whole rendered lines
|
|
1167
|
-
*
|
|
1198
|
+
* Draw whole rendered lines as a float-out: each SGR run starts at a lifted
|
|
1199
|
+
* mix toward the assumed foreground and recedes to the run's own settled
|
|
1200
|
+
* color. At `age >= ramp.length` the lines come back byte-identical, so the
|
|
1201
|
+
* palette sequences win without a snap. The two-level mode overlays faint for
|
|
1202
|
+
* the whole flight; the caller stops invoking this once the clock settles.
|
|
1168
1203
|
*
|
|
1169
|
-
* The
|
|
1170
|
-
* the
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
* its normal intensity in the two-level mode - so the level never leaks past
|
|
1175
|
-
* the line it was applied to. Empty lines come back byte-identical, so
|
|
1176
|
-
* the renderer leaves the blank rows around a card alone.
|
|
1204
|
+
* The mix is opened after every SGR the line already carries, so the status
|
|
1205
|
+
* glyph, the dim body rule, and the bold tool name each recede toward their
|
|
1206
|
+
* own color. Each line ends with the sequence that undoes the overlay, so the
|
|
1207
|
+
* mix never leaks past the line it was applied to. Empty lines come back
|
|
1208
|
+
* byte-identical, so the renderer leaves the blank rows around a card alone.
|
|
1177
1209
|
* @param lines - the rendered lines of the block.
|
|
1178
|
-
* @param age -
|
|
1179
|
-
* @param style - the capability and the ramp.
|
|
1180
|
-
* @param from - first line the
|
|
1210
|
+
* @param age - elapsed time in units of `stepMs`; `ramp.length` is t = 1.
|
|
1211
|
+
* @param style - the capability and the ramp whose last level is the assumed foreground.
|
|
1212
|
+
* @param from - first line the mix is applied to; the lines before it come
|
|
1181
1213
|
* back byte-identical, which is how a caller keeps rows the renderer can no
|
|
1182
1214
|
* longer repaint out of the fade.
|
|
1183
|
-
* @returns the lines at that
|
|
1184
|
-
*
|
|
1215
|
+
* @returns the lines at that mix; copies when the style writes no sequence,
|
|
1216
|
+
* which is what the `none` capability and a settled age yield.
|
|
1185
1217
|
*/
|
|
1186
1218
|
function recolorLines(lines, age, style, from = 0) {
|
|
1187
|
-
const
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
|
|
1219
|
+
const capability = style.capability;
|
|
1220
|
+
if (capability === "none") return [...lines];
|
|
1221
|
+
if (capability === "dim") return lines.map((line, index) => index < from || line === "" ? line : `${DIM}${line}${RESET_INTENSITY}`);
|
|
1222
|
+
const steps = style.ramp.length;
|
|
1223
|
+
const fg = style.ramp[steps - 1];
|
|
1224
|
+
if (fg === void 0 || age >= steps) return [...lines];
|
|
1225
|
+
const t = age / steps;
|
|
1226
|
+
return lines.map((line, index) => index < from || line === "" ? line : floatOutLine(line, t, capability, fg));
|
|
1227
|
+
}
|
|
1228
|
+
/** Assumed terminal white, used only to lift a settled color that is already brighter than `fg`. */
|
|
1229
|
+
const WHITE_RGB = {
|
|
1230
|
+
r: CHANNEL_MAX,
|
|
1231
|
+
g: CHANNEL_MAX,
|
|
1232
|
+
b: CHANNEL_MAX
|
|
1233
|
+
};
|
|
1234
|
+
/** xterm 16-color palette, indices 0..15. */
|
|
1235
|
+
const ANSI16 = [
|
|
1236
|
+
{
|
|
1237
|
+
r: 0,
|
|
1238
|
+
g: 0,
|
|
1239
|
+
b: 0
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
r: 205,
|
|
1243
|
+
g: 0,
|
|
1244
|
+
b: 0
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
r: 0,
|
|
1248
|
+
g: 205,
|
|
1249
|
+
b: 0
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
r: 205,
|
|
1253
|
+
g: 205,
|
|
1254
|
+
b: 0
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
r: 0,
|
|
1258
|
+
g: 0,
|
|
1259
|
+
b: 238
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
r: 205,
|
|
1263
|
+
g: 0,
|
|
1264
|
+
b: 205
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
r: 0,
|
|
1268
|
+
g: 205,
|
|
1269
|
+
b: 205
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
r: 229,
|
|
1273
|
+
g: 229,
|
|
1274
|
+
b: 229
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
r: 127,
|
|
1278
|
+
g: 127,
|
|
1279
|
+
b: 127
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
r: 255,
|
|
1283
|
+
g: 0,
|
|
1284
|
+
b: 0
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
r: 0,
|
|
1288
|
+
g: 255,
|
|
1289
|
+
b: 0
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
r: 255,
|
|
1293
|
+
g: 255,
|
|
1294
|
+
b: 0
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
r: 92,
|
|
1298
|
+
g: 92,
|
|
1299
|
+
b: 255
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
r: 255,
|
|
1303
|
+
g: 0,
|
|
1304
|
+
b: 255
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
r: 0,
|
|
1308
|
+
g: 255,
|
|
1309
|
+
b: 255
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
r: 255,
|
|
1313
|
+
g: 255,
|
|
1314
|
+
b: 255
|
|
1315
|
+
}
|
|
1316
|
+
];
|
|
1317
|
+
/**
|
|
1318
|
+
* Relative luminance of one sRGB color, using the same weights as the grayscale encoder.
|
|
1319
|
+
* @param color - the color.
|
|
1320
|
+
* @returns the weighted sum of the channels.
|
|
1321
|
+
*/
|
|
1322
|
+
function colorLuminance(color) {
|
|
1323
|
+
return LUMINANCE.r * color.r + LUMINANCE.g * color.g + LUMINANCE.b * color.b;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* The color a float-out starts from: the assumed foreground when that is
|
|
1327
|
+
* brighter than the settle, otherwise a mix toward white.
|
|
1328
|
+
* @param settled - the run's color at t = 1.
|
|
1329
|
+
* @param fg - the assumed terminal foreground.
|
|
1330
|
+
* @returns the lifted color at t = 0.
|
|
1331
|
+
*/
|
|
1332
|
+
function liftColor(settled, fg) {
|
|
1333
|
+
return colorLuminance(fg) >= colorLuminance(settled) ? fg : mixFadeColor(settled, WHITE_RGB, LIFT_TOWARD_WHITE);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Apply SGR faint as a channel scale, matching what a dim palette run looks like.
|
|
1337
|
+
* @param color - the undimmed RGB.
|
|
1338
|
+
* @returns the dimmed RGB.
|
|
1339
|
+
*/
|
|
1340
|
+
function dimColor(color) {
|
|
1341
|
+
return {
|
|
1342
|
+
r: Math.round(color.r * DIM_CHANNEL),
|
|
1343
|
+
g: Math.round(color.g * DIM_CHANNEL),
|
|
1344
|
+
b: Math.round(color.b * DIM_CHANNEL)
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* The RGB a run settles at, given the SGR in force and the assumed default foreground.
|
|
1349
|
+
* @param state - the SGR foreground and dim flag.
|
|
1350
|
+
* @param fg - the assumed default foreground.
|
|
1351
|
+
* @returns the settled color the mix recedes toward.
|
|
1352
|
+
*/
|
|
1353
|
+
function settledRgb(state, fg) {
|
|
1354
|
+
const base = state.color ?? fg;
|
|
1355
|
+
return state.dim ? dimColor(base) : base;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Encode one mixed color under a color capability.
|
|
1359
|
+
* @param capability - `truecolor` or `ansi256`.
|
|
1360
|
+
* @param color - the mixed RGB.
|
|
1361
|
+
* @returns the SGR sequence, or empty when the capability cannot encode a mix.
|
|
1362
|
+
*/
|
|
1363
|
+
function encodeMix(capability, color) {
|
|
1364
|
+
return capability === "truecolor" ? truecolorSgr(color) : `${CSI}38;5;${String(nearestAnsi256(color))}m`;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Rebuild one card row as a per-run float-out mix.
|
|
1368
|
+
* @param line - the rendered line.
|
|
1369
|
+
* @param t - 0 at lift, 1 at settle.
|
|
1370
|
+
* @param capability - how the mix is encoded.
|
|
1371
|
+
* @param fg - the assumed default foreground.
|
|
1372
|
+
* @returns the line with each run overlaid, closed by {@link RESET_FOREGROUND}.
|
|
1373
|
+
*/
|
|
1374
|
+
function floatOutLine(line, t, capability, fg) {
|
|
1375
|
+
let out = "";
|
|
1376
|
+
let last = 0;
|
|
1377
|
+
const state = {
|
|
1378
|
+
color: void 0,
|
|
1379
|
+
dim: false
|
|
1380
|
+
};
|
|
1381
|
+
for (const match of line.matchAll(SGR_SEQUENCE)) {
|
|
1382
|
+
const start = match.index;
|
|
1383
|
+
if (start > last) out += paintSegment(line.slice(last, start), state, t, capability, fg);
|
|
1384
|
+
applySgr(state, match[0]);
|
|
1385
|
+
out += match[0];
|
|
1386
|
+
last = start + match[0].length;
|
|
1387
|
+
}
|
|
1388
|
+
if (last < line.length) out += paintSegment(line.slice(last), state, t, capability, fg);
|
|
1389
|
+
return `${out}${RESET_FOREGROUND}`;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Overlay one text run with the float-out mix for the SGR in force.
|
|
1393
|
+
* @param text - the run's characters, with no SGR.
|
|
1394
|
+
* @param state - the SGR in force.
|
|
1395
|
+
* @param t - 0 at lift, 1 at settle.
|
|
1396
|
+
* @param capability - how the mix is encoded.
|
|
1397
|
+
* @param fg - the assumed default foreground.
|
|
1398
|
+
* @returns the run prefixed by intensity-reset and the mixed color.
|
|
1399
|
+
*/
|
|
1400
|
+
function paintSegment(text, state, t, capability, fg) {
|
|
1401
|
+
const settled = settledRgb(state, fg);
|
|
1402
|
+
return `${RESET_INTENSITY}${encodeMix(capability, mixFadeColor(liftColor(settled, fg), settled, t))}${text}`;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* The float-out sequence one reasoning span draws under, toward dimmed `fg`.
|
|
1406
|
+
* @param style - the capability and the ramp.
|
|
1407
|
+
* @param age - elapsed time in units of `stepMs`.
|
|
1408
|
+
* @returns the sequence, or empty once `age` reaches the ramp length.
|
|
1409
|
+
*/
|
|
1410
|
+
function floatOutSpanSgr(style, age) {
|
|
1411
|
+
if (style.capability === "dim") return DIM;
|
|
1412
|
+
if (style.capability !== "truecolor" && style.capability !== "ansi256") return "";
|
|
1413
|
+
const steps = style.ramp.length;
|
|
1414
|
+
const fg = style.ramp[steps - 1];
|
|
1415
|
+
if (fg === void 0 || age >= steps) return "";
|
|
1416
|
+
const settled = dimColor(fg);
|
|
1417
|
+
const mixed = mixFadeColor(liftColor(settled, fg), settled, age / steps);
|
|
1418
|
+
return `${RESET_INTENSITY}${encodeMix(style.capability, mixed)}`;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Apply one SGR sequence to the float-out foreground state.
|
|
1422
|
+
* @param state - the state to update.
|
|
1423
|
+
* @param sequence - a CSI SGR including the trailing `m`.
|
|
1424
|
+
*/
|
|
1425
|
+
function applySgr(state, sequence) {
|
|
1426
|
+
const body = sequence.slice(2, -1);
|
|
1427
|
+
const parts = body === "" ? [0] : body.split(";").map((part) => part === "" ? 0 : Number(part));
|
|
1428
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
1429
|
+
const code = colorParam(parts, index);
|
|
1430
|
+
if (code === 0) {
|
|
1431
|
+
state.color = void 0;
|
|
1432
|
+
state.dim = false;
|
|
1433
|
+
} else if (code === 2) state.dim = true;
|
|
1434
|
+
else if (code === 22) state.dim = false;
|
|
1435
|
+
else if (code === 39) state.color = void 0;
|
|
1436
|
+
else if (code >= 30 && code <= 37) state.color = ANSI16[code - 30];
|
|
1437
|
+
else if (code >= 90 && code <= 97) state.color = ANSI16[code - 90 + 8];
|
|
1438
|
+
else if (code === 38) {
|
|
1439
|
+
const read = readExtendedColor(parts, index);
|
|
1440
|
+
state.color = read.color;
|
|
1441
|
+
index += read.skip;
|
|
1442
|
+
} else if (code === 48) index += readExtendedColor(parts, index).skip;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Consume an ITU T.416 extended color (`38`/`48` plus `2;R;G;B` or `5;N`).
|
|
1447
|
+
* @param parts - the SGR parameter list.
|
|
1448
|
+
* @param index - index of the `38` or `48` code.
|
|
1449
|
+
* @returns the RGB for a foreground read, and how many following parameters were consumed.
|
|
1450
|
+
*/
|
|
1451
|
+
function readExtendedColor(parts, index) {
|
|
1452
|
+
const mode = parts[index + 1];
|
|
1453
|
+
if (mode === 2) return {
|
|
1454
|
+
color: {
|
|
1455
|
+
r: channel(colorParam(parts, index + 2)),
|
|
1456
|
+
g: channel(colorParam(parts, index + 3)),
|
|
1457
|
+
b: channel(colorParam(parts, index + 4))
|
|
1458
|
+
},
|
|
1459
|
+
skip: 4
|
|
1460
|
+
};
|
|
1461
|
+
if (mode === 5) return {
|
|
1462
|
+
color: ansi256Rgb(colorParam(parts, index + 2)),
|
|
1463
|
+
skip: 2
|
|
1464
|
+
};
|
|
1465
|
+
return {
|
|
1466
|
+
color: void 0,
|
|
1467
|
+
skip: mode === void 0 ? 0 : 1
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Read one SGR numeric parameter, defaulting a missing slot to 0.
|
|
1472
|
+
* @param parts - the SGR parameter list.
|
|
1473
|
+
* @param index - the slot to read.
|
|
1474
|
+
* @returns the parameter, or 0 when it is absent.
|
|
1475
|
+
*/
|
|
1476
|
+
function colorParam(parts, index) {
|
|
1477
|
+
return parts[index] ?? 0;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* RGB for one xterm 256-color index.
|
|
1481
|
+
* @param index - 0..255.
|
|
1482
|
+
* @returns the palette color, clamped onto the table.
|
|
1483
|
+
*/
|
|
1484
|
+
function ansi256Rgb(index) {
|
|
1485
|
+
const n = Math.max(0, Math.min(255, Math.floor(index)));
|
|
1486
|
+
if (n < 16) {
|
|
1487
|
+
const color = ANSI16[n];
|
|
1488
|
+
/* v8 ignore next -- ANSI16 has 16 entries and n is already clamped to 0..15 */
|
|
1489
|
+
if (color === void 0) return {
|
|
1490
|
+
r: 0,
|
|
1491
|
+
g: 0,
|
|
1492
|
+
b: 0
|
|
1493
|
+
};
|
|
1494
|
+
return color;
|
|
1495
|
+
}
|
|
1496
|
+
if (n >= GRAY_FIRST_INDEX) {
|
|
1497
|
+
const value = GRAY_FIRST_VALUE + (n - GRAY_FIRST_INDEX) * GRAY_VALUE_STEP;
|
|
1498
|
+
return {
|
|
1499
|
+
r: value,
|
|
1500
|
+
g: value,
|
|
1501
|
+
b: value
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
const cube = n - 16;
|
|
1505
|
+
const r = Math.floor(cube / 36);
|
|
1506
|
+
const g = Math.floor(cube % 36 / 6);
|
|
1507
|
+
const b = cube % 6;
|
|
1508
|
+
const level = (step) => step === 0 ? 0 : 55 + step * 40;
|
|
1509
|
+
return {
|
|
1510
|
+
r: level(r),
|
|
1511
|
+
g: level(g),
|
|
1512
|
+
b: level(b)
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Nearest xterm 256-color index to one RGB, by squared channel distance.
|
|
1517
|
+
* @param color - the mixed RGB.
|
|
1518
|
+
* @returns an index in 0..255.
|
|
1519
|
+
*/
|
|
1520
|
+
function nearestAnsi256(color) {
|
|
1521
|
+
let best = 0;
|
|
1522
|
+
let bestDist = Infinity;
|
|
1523
|
+
for (let n = 0; n < 256; n += 1) {
|
|
1524
|
+
const candidate = ansi256Rgb(n);
|
|
1525
|
+
const dist = (candidate.r - color.r) ** 2 + (candidate.g - color.g) ** 2 + (candidate.b - color.b) ** 2;
|
|
1526
|
+
if (dist < bestDist) {
|
|
1527
|
+
bestDist = dist;
|
|
1528
|
+
best = n;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return best;
|
|
1191
1532
|
}
|
|
1192
1533
|
/**
|
|
1193
1534
|
* The tail of one streaming region - the visible text or the reasoning of one
|
|
@@ -1202,8 +1543,8 @@ function recolorLines(lines, age, style, from = 0) {
|
|
|
1202
1543
|
* Ages are read, never counted. A chunk's age is the elapsed time since it
|
|
1203
1544
|
* became visible divided by `stepMs`, computed at the moment a render asks for
|
|
1204
1545
|
* it, so a render triggered by a delta between two fade periods draws every
|
|
1205
|
-
* word at its own
|
|
1206
|
-
*
|
|
1546
|
+
* word at its own mix and words do not move in lockstep. Fade-in floors that
|
|
1547
|
+
* age onto a ramp slot; float-out uses the fractional value.
|
|
1207
1548
|
*
|
|
1208
1549
|
* Arrival rate changes the tail's length, never its depth: whatever the rate,
|
|
1209
1550
|
* a chunk is at the foreground `steps * stepMs` after it appeared, so a fast
|
|
@@ -1252,7 +1593,7 @@ var FadeTracker = class {
|
|
|
1252
1593
|
* Drop the chunks that reached the step count, which the application runs
|
|
1253
1594
|
* once per fade period so a settled chunk stops being matched against the
|
|
1254
1595
|
* rendered lines.
|
|
1255
|
-
* @returns whether a chunk still
|
|
1596
|
+
* @returns whether a chunk is still younger than `steps * stepMs`, and
|
|
1256
1597
|
* so whether the tail keeps moving after this period.
|
|
1257
1598
|
*/
|
|
1258
1599
|
tick() {
|
|
@@ -1263,11 +1604,10 @@ var FadeTracker = class {
|
|
|
1263
1604
|
}
|
|
1264
1605
|
/**
|
|
1265
1606
|
* Whether the current frame still differs from the settled rendering.
|
|
1266
|
-
* @returns true while a tracked chunk
|
|
1267
|
-
* is the only reason to keep ticking.
|
|
1607
|
+
* @returns true while a tracked chunk is younger than `steps * stepMs`.
|
|
1268
1608
|
*/
|
|
1269
1609
|
needsRepaint() {
|
|
1270
|
-
return this.chunks.some((chunk) => this.ageOf(chunk) < this.steps
|
|
1610
|
+
return this.chunks.some((chunk) => this.ageOf(chunk) < this.steps);
|
|
1271
1611
|
}
|
|
1272
1612
|
/**
|
|
1273
1613
|
* The tail {@link recolorTail} recolors.
|
|
@@ -1290,21 +1630,22 @@ var FadeTracker = class {
|
|
|
1290
1630
|
this.openChunk = void 0;
|
|
1291
1631
|
}
|
|
1292
1632
|
/**
|
|
1293
|
-
* The brightness
|
|
1633
|
+
* The brightness age one chunk draws at right now.
|
|
1294
1634
|
* @param chunk - the tracked chunk.
|
|
1295
|
-
* @returns
|
|
1635
|
+
* @returns elapsed time since it became visible, in units of `stepMs`.
|
|
1296
1636
|
*/
|
|
1297
1637
|
ageOf(chunk) {
|
|
1298
|
-
return
|
|
1638
|
+
return (this.now() - chunk.bornAt) / this.stepMs;
|
|
1299
1639
|
}
|
|
1300
1640
|
};
|
|
1301
1641
|
/**
|
|
1302
|
-
* The age of one whole block that
|
|
1642
|
+
* The age of one whole block that floats out as a unit - a tool card's rows,
|
|
1303
1643
|
* which arrive complete rather than word by word.
|
|
1304
1644
|
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
* the
|
|
1645
|
+
* Progress is `elapsed / (steps * stepMs)`, eased by the same smoothstep the
|
|
1646
|
+
* ramp uses. {@link BlockFadeClock.age} stays defined until that progress
|
|
1647
|
+
* reaches 1, so the last overlay frame can sit near the settled colors and
|
|
1648
|
+
* the next frame is the component's own bytes.
|
|
1308
1649
|
*/
|
|
1309
1650
|
var BlockFadeClock = class {
|
|
1310
1651
|
options;
|
|
@@ -1312,18 +1653,25 @@ var BlockFadeClock = class {
|
|
|
1312
1653
|
this.options = options;
|
|
1313
1654
|
}
|
|
1314
1655
|
/**
|
|
1315
|
-
*
|
|
1316
|
-
* @returns
|
|
1317
|
-
* level and renders in the colors the component itself produced.
|
|
1656
|
+
* How far through the float-out this block is.
|
|
1657
|
+
* @returns 0 at birth, 1 at or after `steps * stepMs`.
|
|
1318
1658
|
*/
|
|
1319
|
-
|
|
1659
|
+
progress() {
|
|
1320
1660
|
const { bornAt, stepMs, steps, now } = this.options;
|
|
1321
|
-
|
|
1322
|
-
|
|
1661
|
+
return Math.min(1, Math.max(0, (now() - bornAt) / (steps * stepMs)));
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* The fractional age the block draws at right now, in units of `stepMs`.
|
|
1665
|
+
* @returns `progress * steps`, or undefined once progress has reached 1 and
|
|
1666
|
+
* the block renders in the colors the component itself produced.
|
|
1667
|
+
*/
|
|
1668
|
+
age() {
|
|
1669
|
+
const t = this.progress();
|
|
1670
|
+
return t >= 1 ? void 0 : t * this.options.steps;
|
|
1323
1671
|
}
|
|
1324
1672
|
/**
|
|
1325
|
-
* Whether this block still draws below the
|
|
1326
|
-
* @returns true while {@link BlockFadeClock.age} yields a
|
|
1673
|
+
* Whether this block still draws below the settled colors.
|
|
1674
|
+
* @returns true while {@link BlockFadeClock.age} yields a value.
|
|
1327
1675
|
*/
|
|
1328
1676
|
needsRepaint() {
|
|
1329
1677
|
return this.age() !== void 0;
|
|
@@ -1376,27 +1724,32 @@ var FadeRegistry = class {
|
|
|
1376
1724
|
*
|
|
1377
1725
|
* Styling inside a recolored run survives. The run reasserts the sequences in
|
|
1378
1726
|
* force at its start, so an enclosing bold or italic continues across it; an
|
|
1379
|
-
* enclosing foreground color reasserted there wins over the ramp, and
|
|
1380
|
-
* simply does not fade. Each recolored line ends with `ESC[39m` (or
|
|
1381
|
-
* in the two-level mode, which also ends bold) after the line's own
|
|
1382
|
-
* sequences, so the recolor never leaks past the line it was applied
|
|
1727
|
+
* enclosing foreground color reasserted there wins over the fade-in ramp, and
|
|
1728
|
+
* that run simply does not fade. Each recolored line ends with `ESC[39m` (or
|
|
1729
|
+
* `ESC[22m` in the two-level mode, which also ends bold) after the line's own
|
|
1730
|
+
* closing sequences, so the recolor never leaks past the line it was applied
|
|
1731
|
+
* to. Float-out resets intensity before the mixed color so a dim wrapper does
|
|
1732
|
+
* not stack on the overlay, and at t >= 1 the original lines come back
|
|
1733
|
+
* unchanged.
|
|
1383
1734
|
* @param lines - the rendered lines of the streaming block, newest text last.
|
|
1384
1735
|
* @param spans - the tail from {@link FadeTracker.spans}, oldest first.
|
|
1385
1736
|
* @param style - the capability and the ramp.
|
|
1386
1737
|
* @param from - first line the tail may recolor; the lines before it come back
|
|
1387
1738
|
* byte-identical, which is how a caller keeps rows the renderer can no longer
|
|
1388
1739
|
* repaint out of the fade.
|
|
1740
|
+
* @param mode - `in` climbs {@link buildFadeRamp}; `out` recedes toward the
|
|
1741
|
+
* dim foreground reasoning settles in. Defaults to `in`.
|
|
1389
1742
|
* @returns the lines with the tail recolored; lines the tail does not cover
|
|
1390
1743
|
* are returned byte-identical, so the renderer leaves them alone.
|
|
1391
1744
|
*/
|
|
1392
|
-
function recolorTail(lines, spans, style, from = 0) {
|
|
1393
|
-
if (
|
|
1745
|
+
function recolorTail(lines, spans, style, from = 0, mode = "in") {
|
|
1746
|
+
if (spans.length === 0 || lines.length === 0) return [...lines];
|
|
1394
1747
|
const cells = cellsFromEnd(lines);
|
|
1395
1748
|
const runs = [];
|
|
1396
1749
|
for (const span of [...spans].reverse()) {
|
|
1397
1750
|
const covered = consumeSpan(cells, span.text);
|
|
1398
1751
|
if (covered === void 0) break;
|
|
1399
|
-
const sgr = fadeSgr(style, span.age);
|
|
1752
|
+
const sgr = mode === "out" ? floatOutSpanSgr(style, span.age) : fadeSgr(style, span.age);
|
|
1400
1753
|
if (sgr !== "") collectRuns(runs, covered, sgr);
|
|
1401
1754
|
}
|
|
1402
1755
|
const byLine = /* @__PURE__ */ new Map();
|
|
@@ -1700,8 +2053,9 @@ var NoticeBlock = class {
|
|
|
1700
2053
|
* durable `assistant/message` replaces both with the committed content.
|
|
1701
2054
|
*
|
|
1702
2055
|
* A block that is streaming right now can carry a {@link FadeRender} for its
|
|
1703
|
-
* text and another for its reasoning
|
|
1704
|
-
*
|
|
2056
|
+
* text and another for its reasoning. Visible text draws newest words dimmed
|
|
2057
|
+
* and brightening; reasoning floats out from a lifted color toward the dim
|
|
2058
|
+
* italic it settles in. A block rebuilt from history carries neither, and
|
|
1705
2059
|
* {@link AssistantBlock.commit} drops the ones a streaming block had, so
|
|
1706
2060
|
* settled text is never recolored.
|
|
1707
2061
|
*/
|
|
@@ -1783,7 +2137,7 @@ var AssistantBlock = class {
|
|
|
1783
2137
|
setRepaintFloor(floor) {
|
|
1784
2138
|
if (floor <= this.repaintFloor) return false;
|
|
1785
2139
|
this.repaintFloor = floor;
|
|
1786
|
-
return
|
|
2140
|
+
return this.fade !== void 0 && fadingInSpans(this.fade).length > 0 || this.reasoningFade !== void 0 && floatingOutSpans(this.reasoningFade).length > 0;
|
|
1787
2141
|
}
|
|
1788
2142
|
/**
|
|
1789
2143
|
* Append streamed visible text.
|
|
@@ -1849,13 +2203,11 @@ var AssistantBlock = class {
|
|
|
1849
2203
|
return withGutter(this.theme.palette, lines, (index) => index >= section.from && index < section.to);
|
|
1850
2204
|
}
|
|
1851
2205
|
/**
|
|
1852
|
-
* The reasoning lines, with the streaming tail
|
|
2206
|
+
* The reasoning lines, with the streaming tail floated out toward dim italic.
|
|
1853
2207
|
*
|
|
1854
|
-
* The
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1857
|
-
* tail therefore arrives at the dim foreground the settled reasoning carries,
|
|
1858
|
-
* not at the plain one.
|
|
2208
|
+
* The mix starts at a lifted color and recedes to the faint foreground the
|
|
2209
|
+
* settled reasoning already carries. At full age the original dim italic
|
|
2210
|
+
* bytes come back unchanged.
|
|
1859
2211
|
* @param width - the width the reasoning lays out in.
|
|
1860
2212
|
* @param at - index of the region's first line in this block's render.
|
|
1861
2213
|
* @returns the lines to draw.
|
|
@@ -1866,7 +2218,7 @@ var AssistantBlock = class {
|
|
|
1866
2218
|
if (fade === void 0) return lines;
|
|
1867
2219
|
if (this.reasoningFadeWidth !== void 0 && this.reasoningFadeWidth !== width) fade.flush();
|
|
1868
2220
|
this.reasoningFadeWidth = width;
|
|
1869
|
-
return recolorTail(lines,
|
|
2221
|
+
return recolorTail(lines, floatingOutSpans(fade), fade.style(), this.repaintFloor - at, "out");
|
|
1870
2222
|
}
|
|
1871
2223
|
/**
|
|
1872
2224
|
* The Markdown lines, with the streaming tail recolored.
|
|
@@ -1885,14 +2237,14 @@ var AssistantBlock = class {
|
|
|
1885
2237
|
if (fade === void 0) return lines;
|
|
1886
2238
|
if (this.fadeWidth !== void 0 && this.fadeWidth !== width) fade.flush();
|
|
1887
2239
|
this.fadeWidth = width;
|
|
1888
|
-
return recolorTail(lines,
|
|
2240
|
+
return recolorTail(lines, fadingInSpans(fade), fade.style(), this.repaintFloor - at);
|
|
1889
2241
|
}
|
|
1890
2242
|
};
|
|
1891
2243
|
/**
|
|
1892
2244
|
* A tool call card: status glyph, tool name, headline, then a foldable body.
|
|
1893
2245
|
*
|
|
1894
|
-
* The card arrives in two pieces, and each
|
|
1895
|
-
* the call rows when the call is logged, the result rows when the tool
|
|
2246
|
+
* The card arrives in two pieces, and each floats out on its own: the header
|
|
2247
|
+
* and the call rows when the call is logged, the result rows when the tool
|
|
1896
2248
|
* answers. A card rebuilt from history carries neither fade.
|
|
1897
2249
|
*/
|
|
1898
2250
|
var ToolBlock = class {
|
|
@@ -1961,7 +2313,7 @@ var ToolBlock = class {
|
|
|
1961
2313
|
}
|
|
1962
2314
|
/**
|
|
1963
2315
|
* Hand the rows the renderer can no longer repaint back to the colors this
|
|
1964
|
-
* card drew them in, so a fade that is still
|
|
2316
|
+
* card drew them in, so a fade that is still moving never rewrites them.
|
|
1965
2317
|
* @param floor - this card's own first repaintable line; the application
|
|
1966
2318
|
* raises it as the frame grows and never lowers it.
|
|
1967
2319
|
* @returns whether the floor took rows away from a fade that is drawing
|
|
@@ -2022,11 +2374,11 @@ var ToolBlock = class {
|
|
|
2022
2374
|
}
|
|
2023
2375
|
};
|
|
2024
2376
|
/**
|
|
2025
|
-
* Draw one group of a card's rows at the
|
|
2377
|
+
* Draw one group of a card's rows at the mix its fade reports.
|
|
2026
2378
|
* @param lines - the group's final rendered lines.
|
|
2027
2379
|
* @param fade - the group's fade; absent for a card that never faded.
|
|
2028
2380
|
* @param from - first line of the group the fade may recolor.
|
|
2029
|
-
* @returns the lines at that
|
|
2381
|
+
* @returns the lines at that mix, or the lines themselves once the fade
|
|
2030
2382
|
* settled or was never attached.
|
|
2031
2383
|
*/
|
|
2032
2384
|
function faded(lines, fade, from) {
|
|
@@ -2035,18 +2387,30 @@ function faded(lines, fade, from) {
|
|
|
2035
2387
|
return age === void 0 ? lines : recolorLines(lines, age, fade.style(), from);
|
|
2036
2388
|
}
|
|
2037
2389
|
/**
|
|
2038
|
-
* The tail chunks one streaming
|
|
2390
|
+
* The tail chunks one streaming reply still draws below the terminal's own
|
|
2039
2391
|
* foreground.
|
|
2040
2392
|
*
|
|
2041
2393
|
* The last ramp level is an assumed foreground - pi-tui reports the terminal
|
|
2042
2394
|
* background but not its foreground - so a chunk that reached it is left to
|
|
2043
2395
|
* draw in the terminal's own foreground, which is also what it draws in once it
|
|
2044
2396
|
* leaves the tail. No chunk can therefore jump color as it settles.
|
|
2045
|
-
* @param fade - the region's fade
|
|
2397
|
+
* @param fade - the region's fade.
|
|
2046
2398
|
* @returns the spans to recolor, oldest first.
|
|
2047
2399
|
*/
|
|
2048
|
-
function
|
|
2049
|
-
return fade
|
|
2400
|
+
function fadingInSpans(fade) {
|
|
2401
|
+
return fade.spans().filter((span) => span.age < fade.steps - 1);
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* The tail chunks one streaming reasoning region still floats out.
|
|
2405
|
+
*
|
|
2406
|
+
* Overlay continues until `age >= steps`, which is `steps * stepMs` after the
|
|
2407
|
+
* word appeared. The last overlay sits near the dim settle; the next frame is
|
|
2408
|
+
* the original dim italic bytes.
|
|
2409
|
+
* @param fade - the region's fade.
|
|
2410
|
+
* @returns the spans to recolor, oldest first.
|
|
2411
|
+
*/
|
|
2412
|
+
function floatingOutSpans(fade) {
|
|
2413
|
+
return fade.spans().filter((span) => span.age < fade.steps);
|
|
2050
2414
|
}
|
|
2051
2415
|
//#endregion
|
|
2052
2416
|
//#region lib/types/completion.js
|
|
@@ -2304,7 +2668,7 @@ const OFF_SCREEN = " · off screen";
|
|
|
2304
2668
|
/** The keys the inspector answers, drawn dim under its rows. */
|
|
2305
2669
|
const HINT = "↑ ↓ blocks · ← → parts · Enter page · Esc back";
|
|
2306
2670
|
/** What ends a line the width cut short; one column, so the mark itself fits. */
|
|
2307
|
-
const ELLIPSIS = "…";
|
|
2671
|
+
const ELLIPSIS$1 = "…";
|
|
2308
2672
|
/**
|
|
2309
2673
|
* Draw the focused section.
|
|
2310
2674
|
* @param view - the section to show.
|
|
@@ -2321,7 +2685,7 @@ function renderInspector(view, render) {
|
|
|
2321
2685
|
const wrapped = view.rows.flatMap((row) => wrapTextWithAnsi(row, width));
|
|
2322
2686
|
lines.push(...foldRows(wrapped, render.previewLines, (hidden) => palette.dim(`… ${String(hidden)} more row${hidden === 1 ? "" : "s"} · Enter opens the page`)));
|
|
2323
2687
|
lines.push(palette.dim(HINT));
|
|
2324
|
-
return lines.map((line) => truncateToWidth(line, width, ELLIPSIS));
|
|
2688
|
+
return lines.map((line) => truncateToWidth(line, width, ELLIPSIS$1));
|
|
2325
2689
|
}
|
|
2326
2690
|
/**
|
|
2327
2691
|
* The heading line. A tool title can run past any terminal, so the subject is
|
|
@@ -2336,7 +2700,7 @@ function heading(view, palette, width) {
|
|
|
2336
2700
|
const subject = palette.bold(palette.accent(view.heading));
|
|
2337
2701
|
if (view.highlighted) return subject;
|
|
2338
2702
|
const suffix = palette.dim(OFF_SCREEN);
|
|
2339
|
-
return `${truncateToWidth(subject, Math.max(1, width - visibleWidth(suffix)), ELLIPSIS)}${suffix}`;
|
|
2703
|
+
return `${truncateToWidth(subject, Math.max(1, width - visibleWidth(suffix)), ELLIPSIS$1)}${suffix}`;
|
|
2340
2704
|
}
|
|
2341
2705
|
/**
|
|
2342
2706
|
* Draw the parts of the focused block as one row of choices.
|
|
@@ -2943,10 +3307,11 @@ function plural(count, noun) {
|
|
|
2943
3307
|
* The status bar under the editor: plain session facts become an ordered list
|
|
2944
3308
|
* of segments, each with a stable id, the short label the bar draws, and what
|
|
2945
3309
|
* `Enter` on it does — the rows the app prints, or the app's own navigable
|
|
2946
|
-
* page; a second function renders
|
|
2947
|
-
*
|
|
2948
|
-
* the
|
|
2949
|
-
* terminal, and no clock: elapsed values arrive already formatted
|
|
3310
|
+
* page; a second function renders those segments as one unfocused line of key
|
|
3311
|
+
* facts, or as two focused lines whose first is a sliding window that always
|
|
3312
|
+
* includes the selected segment. Everything here is pure — no Context, no
|
|
3313
|
+
* services, no terminal, and no clock: elapsed values arrive already formatted,
|
|
3314
|
+
* and the terminal width arrives as an input so every line fits it.
|
|
2950
3315
|
* @module @deepseek-ai/dsh-tui-app/footer
|
|
2951
3316
|
*/
|
|
2952
3317
|
/** The segment focus enters the bar on; `buildFooterSegments` always emits it first. */
|
|
@@ -2962,10 +3327,20 @@ const SEPARATOR$1 = " · ";
|
|
|
2962
3327
|
const WORKSPACE_LABEL_WIDTH = 24;
|
|
2963
3328
|
/** The last two segments of a path, with the separators that precede them. */
|
|
2964
3329
|
const PATH_TAIL = /[/\\][^/\\]+[/\\][^/\\]+$/u;
|
|
2965
|
-
/** The keys the focused bar answers,
|
|
3330
|
+
/** The keys the focused bar answers, drawn on the expansion line. */
|
|
2966
3331
|
const FOCUS_HINTS$1 = `← → select${SEPARATOR$1}↑ ↓ regions${SEPARATOR$1}Enter details${SEPARATOR$1}Esc back`;
|
|
2967
|
-
/**
|
|
2968
|
-
const ENTRY_HINT =
|
|
3332
|
+
/** The one entry key the unfocused line keeps, trailing the facts. */
|
|
3333
|
+
const ENTRY_HINT = "Shift+↓";
|
|
3334
|
+
/** What ends a line the width cut short; one column, so the mark itself fits. */
|
|
3335
|
+
const ELLIPSIS = "…";
|
|
3336
|
+
/** Facts the unfocused line keeps; every other present segment folds into `+N`. */
|
|
3337
|
+
const KEY_SEGMENT_IDS = new Set([
|
|
3338
|
+
"model",
|
|
3339
|
+
"effort",
|
|
3340
|
+
"turn",
|
|
3341
|
+
"context",
|
|
3342
|
+
"workspace"
|
|
3343
|
+
]);
|
|
2969
3344
|
/** The command that prints every projection section at once. */
|
|
2970
3345
|
const STATUS_COMMAND_ROW = "/status prints all of these sections";
|
|
2971
3346
|
/**
|
|
@@ -3016,10 +3391,24 @@ function startsWithDirectory(path, directory) {
|
|
|
3016
3391
|
return next === "" || next === "/" || next === "\\";
|
|
3017
3392
|
}
|
|
3018
3393
|
/**
|
|
3394
|
+
* The effort segment: always present, labelled `effort default` when the
|
|
3395
|
+
* selection leaves reasoning effort to the model.
|
|
3396
|
+
* @param effort - the selection's reasoning effort; absent is the model's own default.
|
|
3397
|
+
* @returns the segment.
|
|
3398
|
+
*/
|
|
3399
|
+
function effortSegment(effort) {
|
|
3400
|
+
const unset = effort === void 0;
|
|
3401
|
+
return {
|
|
3402
|
+
id: "effort",
|
|
3403
|
+
label: unset ? "effort default" : `effort ${effort}`,
|
|
3404
|
+
detail: printed([unset ? "reasoning effort: the model's own default" : `reasoning effort: ${effort}`, `Shift+Tab cycles it${SEPARATOR$1}/effort picks one`])
|
|
3405
|
+
};
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3019
3408
|
* Build the status bar's segments in the order it draws them: model, effort,
|
|
3020
|
-
* permission, turn, usage, the projection facts, workspace, attachments.
|
|
3021
|
-
*
|
|
3022
|
-
* needs its fact.
|
|
3409
|
+
* permission, turn, usage, the projection facts, workspace, attachments. The
|
|
3410
|
+
* model, effort, and workspace segments are always present; every other
|
|
3411
|
+
* segment needs its fact.
|
|
3023
3412
|
* @param inputs - the facts the app read for the bound session.
|
|
3024
3413
|
* @returns the segments, the model segment first.
|
|
3025
3414
|
*/
|
|
@@ -3035,12 +3424,7 @@ function buildFooterSegments(inputs) {
|
|
|
3035
3424
|
effort === void 0 ? "reasoning effort: the model's own default" : `reasoning effort: ${effort}`,
|
|
3036
3425
|
"/model picks the provider and model for the next request"
|
|
3037
3426
|
])
|
|
3038
|
-
}];
|
|
3039
|
-
if (effort !== void 0) segments.push({
|
|
3040
|
-
id: "effort",
|
|
3041
|
-
label: `effort ${effort}`,
|
|
3042
|
-
detail: printed([`reasoning effort: ${effort}`, `Shift+Tab cycles it${SEPARATOR$1}/effort picks one`])
|
|
3043
|
-
});
|
|
3427
|
+
}, effortSegment(effort)];
|
|
3044
3428
|
if (inputs.permission !== void 0) {
|
|
3045
3429
|
const projected = permissionLines(facts);
|
|
3046
3430
|
segments.push({
|
|
@@ -3099,20 +3483,142 @@ function footerSelectionIndex(segments, selected) {
|
|
|
3099
3483
|
return index === -1 ? 0 : index;
|
|
3100
3484
|
}
|
|
3101
3485
|
/**
|
|
3102
|
-
*
|
|
3103
|
-
*
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
3486
|
+
* Inclusive span of `widths` that contains `selected` and fits `budget`.
|
|
3487
|
+
* @param widths - visible width of each part, in order.
|
|
3488
|
+
* @param selected - the index that must stay in the span.
|
|
3489
|
+
* @param budget - maximum visible width of the joined span.
|
|
3490
|
+
* @param sep - visible width of the separator between two parts.
|
|
3491
|
+
* @returns start and end indices, inclusive.
|
|
3492
|
+
*/
|
|
3493
|
+
function windowSpan(widths, selected, budget, sep) {
|
|
3494
|
+
let start = selected;
|
|
3495
|
+
let end = selected;
|
|
3496
|
+
let used = widths[selected];
|
|
3497
|
+
if (used >= budget) return {
|
|
3498
|
+
start,
|
|
3499
|
+
end
|
|
3500
|
+
};
|
|
3501
|
+
while (end + 1 < widths.length) {
|
|
3502
|
+
const next = used + sep + widths[end + 1];
|
|
3503
|
+
if (next > budget) break;
|
|
3504
|
+
end += 1;
|
|
3505
|
+
used = next;
|
|
3506
|
+
}
|
|
3507
|
+
while (start > 0) {
|
|
3508
|
+
const next = used + sep + widths[start - 1];
|
|
3509
|
+
if (next > budget) break;
|
|
3510
|
+
start -= 1;
|
|
3511
|
+
used = next;
|
|
3512
|
+
}
|
|
3513
|
+
return {
|
|
3514
|
+
start,
|
|
3515
|
+
end
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
/**
|
|
3519
|
+
* Cut `line` to `width` columns.
|
|
3520
|
+
* @param line - the already-styled line.
|
|
3521
|
+
* @param width - terminal columns the line must fit; at least 1.
|
|
3522
|
+
* @returns the line, ellipsized when it was wider.
|
|
3523
|
+
*/
|
|
3524
|
+
function fitLine(line, width) {
|
|
3525
|
+
return truncateToWidth(line, Math.max(1, width), ELLIPSIS);
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* The unfocused line: key facts, a `+N` token for the rest, and the entry key.
|
|
3529
|
+
* @param segments - the segments in bar order.
|
|
3530
|
+
* @param palette - the palette the line is dimmed with.
|
|
3531
|
+
* @param width - terminal columns the line must fit.
|
|
3532
|
+
* @returns one dim line.
|
|
3533
|
+
*/
|
|
3534
|
+
function unfocusedLine(segments, palette, width) {
|
|
3535
|
+
const key = segments.filter((segment) => KEY_SEGMENT_IDS.has(segment.id)).map((segment) => segment.label);
|
|
3536
|
+
const folded = segments.length - key.length;
|
|
3537
|
+
if (folded > 0) key.push(`+${String(folded)}`);
|
|
3538
|
+
key.push(ENTRY_HINT);
|
|
3539
|
+
return fitLine(palette.dim(key.join(SEPARATOR$1)), width);
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* The first detail row of a printed segment, or the label of a page segment.
|
|
3543
|
+
* @param segment - the selected segment; absent when the bar has none.
|
|
3544
|
+
* @returns the expansion body.
|
|
3545
|
+
*/
|
|
3546
|
+
function expansionBody(segment) {
|
|
3547
|
+
if (segment === void 0) return "";
|
|
3548
|
+
if (segment.detail.kind === "page") return segment.label;
|
|
3549
|
+
return segment.detail.rows[0];
|
|
3550
|
+
}
|
|
3551
|
+
/**
|
|
3552
|
+
* The focused second line: the selected segment's summary plus the navigation keys.
|
|
3553
|
+
* @param segment - the selected segment.
|
|
3554
|
+
* @param palette - the palette the line is dimmed with.
|
|
3555
|
+
* @param width - terminal columns the line must fit.
|
|
3556
|
+
* @returns one dim line that keeps the hints when they fit.
|
|
3557
|
+
*/
|
|
3558
|
+
function expansionLine(segment, palette, width) {
|
|
3559
|
+
const body = expansionBody(segment);
|
|
3560
|
+
const suffix = palette.dim(body === "" ? FOCUS_HINTS$1 : `${SEPARATOR$1}${FOCUS_HINTS$1}`);
|
|
3561
|
+
const prefix = palette.dim(body);
|
|
3562
|
+
const rest = width - visibleWidth(suffix);
|
|
3563
|
+
if (rest < 1) return fitLine(`${prefix}${suffix}`, width);
|
|
3564
|
+
return `${truncateToWidth(prefix, rest, ELLIPSIS)}${suffix}`;
|
|
3565
|
+
}
|
|
3566
|
+
/**
|
|
3567
|
+
* The focused bar: a sliding window of segment labels, then the expansion line.
|
|
3568
|
+
* @param segments - the segments in bar order.
|
|
3569
|
+
* @param selected - index of the held segment.
|
|
3570
|
+
* @param palette - the palette the labels and hints are styled with.
|
|
3571
|
+
* @param width - terminal columns each line must fit.
|
|
3572
|
+
* @returns two lines, the selected label always in the first.
|
|
3573
|
+
*/
|
|
3574
|
+
function focusedLines(segments, selected, palette, width) {
|
|
3575
|
+
const held = Math.min(Math.max(0, selected), Math.max(0, segments.length - 1));
|
|
3576
|
+
const styled = segments.map((segment, index) => index === held ? palette.bold(palette.accent(segment.label)) : palette.dim(segment.label));
|
|
3577
|
+
const sep = palette.dim(SEPARATOR$1);
|
|
3578
|
+
const { start, end } = windowSpan(styled.map(visibleWidth), held, width, visibleWidth(sep));
|
|
3579
|
+
return [fitLine(styled.slice(start, end + 1).join(sep), width), expansionLine(segments[held], palette, width)];
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Render the footer. Unfocused it is one dim line of key facts with a trailing
|
|
3583
|
+
* `Shift+↓`; focused it is the navigable segments (windowed to `width` so the
|
|
3584
|
+
* selected one is never dropped) and an expansion of that segment plus the
|
|
3585
|
+
* navigation keys. Every returned line fits `width`.
|
|
3106
3586
|
* @param segments - the segments in bar order.
|
|
3107
|
-
* @param render - the palette,
|
|
3108
|
-
* @returns the footer
|
|
3587
|
+
* @param render - the palette, optional selected index, and terminal width.
|
|
3588
|
+
* @returns the footer lines, one when unfocused and two when focused.
|
|
3109
3589
|
*/
|
|
3110
3590
|
function renderFooter(segments, render) {
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
return `${labels.map((label, index) => index === selected ? palette.bold(palette.accent(label)) : palette.dim(label)).join(palette.dim(SEPARATOR$1))}\n${palette.dim(FOCUS_HINTS$1)}`;
|
|
3591
|
+
const width = Math.max(1, render.width);
|
|
3592
|
+
if (render.selected === void 0) return [unfocusedLine(segments, render.palette, width)];
|
|
3593
|
+
return focusedLines(segments, render.selected, render.palette, width);
|
|
3115
3594
|
}
|
|
3595
|
+
/**
|
|
3596
|
+
* The status bar as a mounted component. The view is read once per render
|
|
3597
|
+
* rather than pushed in, so the terminal width of that frame windows and
|
|
3598
|
+
* truncates the lines.
|
|
3599
|
+
*/
|
|
3600
|
+
var FooterBar = class {
|
|
3601
|
+
view;
|
|
3602
|
+
/**
|
|
3603
|
+
* @param view - reads the segments and selection the bar draws.
|
|
3604
|
+
*/
|
|
3605
|
+
constructor(view) {
|
|
3606
|
+
this.view = view;
|
|
3607
|
+
}
|
|
3608
|
+
invalidate() {}
|
|
3609
|
+
/**
|
|
3610
|
+
* Draw the status bar at `width`.
|
|
3611
|
+
* @param width - the total width the bar lays out in.
|
|
3612
|
+
* @returns the footer lines, each no wider than `width`.
|
|
3613
|
+
*/
|
|
3614
|
+
render(width) {
|
|
3615
|
+
const { segments, render } = this.view();
|
|
3616
|
+
return renderFooter(segments, {
|
|
3617
|
+
...render,
|
|
3618
|
+
width
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3116
3622
|
//#endregion
|
|
3117
3623
|
//#region lib/types/prompts.js
|
|
3118
3624
|
/**
|
|
@@ -3835,18 +4341,25 @@ function subagentPanelView(inputs) {
|
|
|
3835
4341
|
};
|
|
3836
4342
|
}
|
|
3837
4343
|
/**
|
|
3838
|
-
* Render the panel
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
3841
|
-
* while the panel holds
|
|
4344
|
+
* Render the panel. Unfocused it is one dim summary line — the listed count
|
|
4345
|
+
* and the first child's key label. Focused it is the heading with navigation
|
|
4346
|
+
* keys, one line per drawn row, the overflow count, and the last listing
|
|
4347
|
+
* failure. The selected row is accented while the panel holds the keyboard.
|
|
3842
4348
|
* @param view - the rows one draw produced.
|
|
3843
4349
|
* @param render - the palette, the selected row, and the listing failure.
|
|
3844
|
-
* @returns the panel text, one line per row.
|
|
4350
|
+
* @returns the panel text, one line when unfocused and one line per row when focused.
|
|
3845
4351
|
*/
|
|
3846
4352
|
function renderSubagentPanel(view, render) {
|
|
3847
4353
|
const { palette, selected } = render;
|
|
3848
4354
|
const total = view.rows.length + view.hidden;
|
|
3849
|
-
|
|
4355
|
+
if (selected === void 0) {
|
|
4356
|
+
const first = view.rows[0]?.text.trimStart().split(SEPARATOR)[0];
|
|
4357
|
+
const parts = [`subagents${SEPARATOR}${String(total)} listed`];
|
|
4358
|
+
if (first) parts.push(first);
|
|
4359
|
+
if (render.failure !== void 0) parts.push(`listing failed: ${render.failure}`);
|
|
4360
|
+
return palette.dim(parts.join(SEPARATOR));
|
|
4361
|
+
}
|
|
4362
|
+
const heading = `subagents${SEPARATOR}${String(total)} listed${SEPARATOR}${FOCUS_HINTS}`;
|
|
3850
4363
|
const lines = [palette.dim(heading)];
|
|
3851
4364
|
for (const [index, row] of view.rows.entries()) lines.push(index === selected ? palette.bold(palette.accent(row.text)) : palette.dim(row.text));
|
|
3852
4365
|
if (view.hidden > 0) lines.push(palette.dim(`+${String(view.hidden)} more${SEPARATOR}/subagents lists them all`));
|
|
@@ -3863,8 +4376,8 @@ function renderSubagentPanel(view, render) {
|
|
|
3863
4376
|
* the editor it keeps two docked regions the keyboard can take over — the
|
|
3864
4377
|
* subagent panel and the status bar — and one repeating tick advances their
|
|
3865
4378
|
* elapsed counters and re-reads a stale subagent listing. A second tick, at
|
|
3866
|
-
* its own period, brightens
|
|
3867
|
-
*
|
|
4379
|
+
* its own period, brightens streamed reply text and floats out reasoning and
|
|
4380
|
+
* tool cards that just landed, each only as far up the frame as the
|
|
3868
4381
|
* renderer repaints without discarding the terminal's scrollback
|
|
3869
4382
|
* (`./screen.ts`).
|
|
3870
4383
|
* @module @deepseek-ai/dsh-tui-app/app
|
|
@@ -4149,7 +4662,13 @@ var TuiApp = class {
|
|
|
4149
4662
|
this.onSubmit(text);
|
|
4150
4663
|
};
|
|
4151
4664
|
this.panel = new Text("", 0, 0);
|
|
4152
|
-
this.footer = new
|
|
4665
|
+
this.footer = new FooterBar(() => ({
|
|
4666
|
+
segments: this.segments,
|
|
4667
|
+
render: {
|
|
4668
|
+
palette: this.deps.palette,
|
|
4669
|
+
...this.focus === "bar" ? { selected: footerSelectionIndex(this.segments, this.barSelection) } : {}
|
|
4670
|
+
}
|
|
4671
|
+
}));
|
|
4153
4672
|
this.modals = new ModalQueue({
|
|
4154
4673
|
tui: this.tui,
|
|
4155
4674
|
slot: this.modalSlot,
|
|
@@ -4393,7 +4912,6 @@ var TuiApp = class {
|
|
|
4393
4912
|
this.tui.requestRender();
|
|
4394
4913
|
}
|
|
4395
4914
|
refreshFooter() {
|
|
4396
|
-
const palette = this.deps.palette;
|
|
4397
4915
|
const permission = this.deps.ctx.get("permissionPresets")?.current(this.agent.session);
|
|
4398
4916
|
const started = this.turnStartedAt;
|
|
4399
4917
|
const inbox = this.agent.inbox;
|
|
@@ -4416,12 +4934,6 @@ var TuiApp = class {
|
|
|
4416
4934
|
kind: attachment.block.type
|
|
4417
4935
|
}))
|
|
4418
4936
|
});
|
|
4419
|
-
const hints = this.agent.status === "running" ? "Enter queues for the next turn · Ctrl+S steers this turn · Esc stops it · Ctrl+O tool output · Ctrl+C twice quits" : "Enter sends · Esc stops the turn · Ctrl+O tool output · Ctrl+C twice quits";
|
|
4420
|
-
this.footer.setText(renderFooter(this.segments, {
|
|
4421
|
-
palette,
|
|
4422
|
-
...this.focus === "bar" ? { selected: footerSelectionIndex(this.segments, this.barSelection) } : {},
|
|
4423
|
-
hints
|
|
4424
|
-
}));
|
|
4425
4937
|
this.tui.requestRender();
|
|
4426
4938
|
}
|
|
4427
4939
|
/**
|
|
@@ -4886,7 +5398,8 @@ var TuiApp = class {
|
|
|
4886
5398
|
}
|
|
4887
5399
|
/**
|
|
4888
5400
|
* Answer one key while the status bar holds focus. Every key is consumed
|
|
4889
|
-
* here, so nothing typed at the bar reaches the editor.
|
|
5401
|
+
* here, so nothing typed at the bar reaches the editor. `Shift+Left` and
|
|
5402
|
+
* `Shift+Right` move between segments the same way as `Left` and `Right`.
|
|
4890
5403
|
* @param data - the raw key bytes.
|
|
4891
5404
|
* @returns the consume marker the input listener returns.
|
|
4892
5405
|
*/
|
|
@@ -4898,11 +5411,11 @@ var TuiApp = class {
|
|
|
4898
5411
|
else this.focusTranscript();
|
|
4899
5412
|
return { consume: true };
|
|
4900
5413
|
}
|
|
4901
|
-
if (matchesKey(data, "left") || matchesKey(data, "shift+tab")) {
|
|
5414
|
+
if (matchesKey(data, "left") || matchesKey(data, "shift+left") || matchesKey(data, "shift+tab")) {
|
|
4902
5415
|
this.moveStatusBar(-1);
|
|
4903
5416
|
return { consume: true };
|
|
4904
5417
|
}
|
|
4905
|
-
if (matchesKey(data, "right") || matchesKey(data, "tab")) {
|
|
5418
|
+
if (matchesKey(data, "right") || matchesKey(data, "shift+right") || matchesKey(data, "tab")) {
|
|
4906
5419
|
this.moveStatusBar(1);
|
|
4907
5420
|
return { consume: true };
|
|
4908
5421
|
}
|
|
@@ -5297,7 +5810,7 @@ var TuiApp = class {
|
|
|
5297
5810
|
"Esc stops the running turn · Ctrl+O expands or collapses tool output",
|
|
5298
5811
|
"Shift+Tab cycles the current model's reasoning effort for the next request",
|
|
5299
5812
|
"Shift+Up focuses the transcript, Shift+Down the subagent panel or the status bar",
|
|
5300
|
-
"Then ↑ ↓ move between blocks, panel rows, and the bar; ← → move between a block's parts or the bar's segments",
|
|
5813
|
+
"Then ↑ ↓ move between blocks, panel rows, and the bar; ← → move between a block's parts or the bar's segments (Shift+← → also move the bar)",
|
|
5301
5814
|
"Enter opens the focused section or segment, Esc returns to the input",
|
|
5302
5815
|
"Ctrl+C clears the input (twice quits) · Ctrl+D on an empty input quits"
|
|
5303
5816
|
];
|
|
@@ -5762,9 +6275,9 @@ var TuiApp = class {
|
|
|
5762
6275
|
this.updateFadeTicker();
|
|
5763
6276
|
}
|
|
5764
6277
|
/**
|
|
5765
|
-
* Take one reasoning delta, which
|
|
5766
|
-
* and the visible text of one message stream at different times and each
|
|
5767
|
-
*
|
|
6278
|
+
* Take one reasoning delta, which floats out on its own tail: the reasoning
|
|
6279
|
+
* and the visible text of one message stream at different times, and each
|
|
6280
|
+
* ages from the moment its own words appeared.
|
|
5768
6281
|
* @param delta - the streamed reasoning delta.
|
|
5769
6282
|
*/
|
|
5770
6283
|
appendStreamedReasoning(delta) {
|
package/package.json
CHANGED