@itookit/dsht 0.2.4 → 0.3.0
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 +2 -2
- package/README.md +26 -12
- package/README.zh.md +26 -12
- package/dist/app.js +121 -62
- package/dist/client.d.ts +8 -1
- package/dist/client.js +21 -4
- package/dist/controller.d.ts +18 -2
- package/dist/controller.js +41 -3
- package/dist/export.d.ts +9 -0
- package/dist/export.js +52 -0
- package/dist/history.d.ts +1 -1
- package/dist/history.js +2 -2
- package/dist/telemetry.d.ts +11 -0
- package/dist/telemetry.js +17 -3
- package/dsht-m.png +0 -0
- package/package.json +2 -2
- package/dsht.png +0 -0
package/README.i18n.yaml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Git blob hashes of the reviewed bilingual pair.
|
|
2
|
-
README.md:
|
|
3
|
-
README.zh.md:
|
|
2
|
+
README.md: d32705049e92256cf72e71aee185e0c2adb0afac
|
|
3
|
+
README.zh.md: 83d5fd715979a84eeb16e1cd30fe306f0778353e
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
> **dsht — Control DeepSeek Harness from any terminal, anywhere.**
|
|
8
8
|
|
|
@@ -81,7 +81,7 @@ After SSHing from a phone into the remote terminal running `dsht`, you can:
|
|
|
81
81
|
|
|
82
82
|
- watch running tasks and live output;
|
|
83
83
|
- read assistant replies, reasoning, and tool status;
|
|
84
|
-
- send a new prompt
|
|
84
|
+
- send a new prompt, automatically steering an active task;
|
|
85
85
|
- approve with `/allow` or reject with `/deny`;
|
|
86
86
|
- answer questions Harness asks;
|
|
87
87
|
- stop the current turn with `/cancel`;
|
|
@@ -287,9 +287,9 @@ JSON output is `{ "items": [...] }`; omit `--json` for tab-separated output. Wor
|
|
|
287
287
|
|
|
288
288
|
## Conversation controls
|
|
289
289
|
|
|
290
|
-
Enter submits a prompt. Ctrl+C clears a non-empty draft first; otherwise it requests cancellation while the selected session is running and exits only when it is idle; repeated keys share an in-flight cancellation. Cancellation waits for a pending prompt admission, and failures keep the client open. Esc sends an explicit cancellation from the conversation even when the cached running flag is idle; open menus also cancel a known running agent while closing. Active
|
|
290
|
+
Enter submits a prompt: while the agent is Working it becomes steering for the next step; while idle it starts a new turn. Steering waits for the current step, including its tools, to finish and does not interrupt a running tool. Ctrl+C clears a non-empty draft first; otherwise it requests cancellation while the selected session is running and exits only when it is idle; repeated keys share an in-flight cancellation. Cancellation waits for a pending prompt admission, and failures keep the client open. Esc sends an explicit cancellation from the conversation even when the cached running flag is idle; open menus also cancel a known running agent while closing. Active history/search/cost loads, host commands, and exports are cancelled first. Page Up/Down scroll the retained transcript; `/older` loads an earlier page. Every exit path, including `/quit` and SIGTERM, stops the selected turn before the connection closes, so quitting does not leave the agent running; an idle session is left untouched. Cancellation leaves pending queue items intact.
|
|
291
291
|
|
|
292
|
-
`/copy`, Ctrl+S, or an unmodified left click in the normal chat view freezes the display and disables mouse reporting for native terminal selection. Esc, Ctrl+S, or Ctrl+C leaves copy mode and catches up with the latest output; leaving copy mode does not cancel the agent. Dialogs and pickers
|
|
292
|
+
`/copy`, Ctrl+S, or an unmodified left click in the normal chat view freezes the display and disables mouse reporting for native terminal selection. Esc, Ctrl+S, or Ctrl+C leaves copy mode and catches up with the latest output; leaving copy mode does not cancel the agent. Dialogs and pickers pause automatic background title, conversation, and status updates. The conversation remains above the composer; mouse wheel and PgUp/PgDn scroll its history without moving the selected option. In the help panel PgUp/PgDn changes help pages. Dialog clicks do not enter copy mode; Ctrl+S freezes the whole display and releases mouse capture for native selection. The Working clock also pauses while reading older history. Help/status/cost panels no longer expire on a timer. Background reception and memory reclamation continue; window resizing can redraw the screen.
|
|
293
293
|
|
|
294
294
|
The mouse wheel and Page Up/Down scroll conversation history; scrolling to the top automatically requests an older page. New output preserves a scrolled reading position; scrolling back down resumes following the newest output. Mouse reporting is enabled while the TUI is mounted and disabled on exit; the terminal must support SGR mouse reports. Esc or Ctrl+C cancels a history load or search before interrupting the remote agent.
|
|
295
295
|
|
|
@@ -301,7 +301,7 @@ In `/ws` and `/resume` pickers, select a workspace or session and press `d` or D
|
|
|
301
301
|
|
|
302
302
|
Within each User group, only the first assistant prose or reasoning message shows an Assistant heading. Later messages and live output reuse that heading across tool results and Context messages. A newly loaded history window starts its own visible group; message sequences, tool status, search, and reasoning expansion remain independent.
|
|
303
303
|
|
|
304
|
-
For mouse copying, click once to enter copy mode, then drag to select after the display freezes. Releasing the mouse keeps the display frozen until Esc, Ctrl+S, or Ctrl+C resumes it. Terminal-native Shift-drag may bypass application mouse reports; press Ctrl+S first in that case.
|
|
304
|
+
For mouse copying, click once to enter copy mode, then drag to select after the display freezes. Releasing the mouse keeps the display frozen until Esc, Ctrl+S, or Ctrl+C resumes it. Terminal-native Shift-drag may bypass application mouse reports; press Ctrl+S first in that case. In dialogs, press Ctrl+S to freeze the entire display and release mouse capture before native selection.
|
|
305
305
|
|
|
306
306
|
Tab completes the leading slash command, extending an ambiguous draft to the shared prefix. The single-line composer supports Readline-style editing. Words are whitespace-delimited; cursor movement and character deletion preserve composed Unicode characters. Multiline pasted text becomes one line with spaces. Ctrl+D on empty input does not exit; Ctrl+C clears a non-empty draft before it stops or exits. Other unhandled modifier shortcuts do not insert their control characters. Both BS and DEL terminal backspace encodings delete backward; the dedicated Delete key (CSI 3~) deletes forward.
|
|
307
307
|
|
|
@@ -326,8 +326,14 @@ Tab completes the leading slash command, extending an ambiguous draft to the sha
|
|
|
326
326
|
| `/resume all` | Show sessions from every workspace |
|
|
327
327
|
| `/model [provider model [effort]]` | Choose a model and its reasoning effort, or submit exact route IDs |
|
|
328
328
|
| `/new` | Create a session in the selected workspace |
|
|
329
|
+
| `/compact` | Compact older history while idle and show the host result |
|
|
329
330
|
| `/cancel` | Cancel the active turn; leave pending queue items intact |
|
|
330
|
-
| `/
|
|
331
|
+
| `/queue` | View pending input; arrows select, Enter / d / Delete removes |
|
|
332
|
+
| `/plan [off|message]` | Enter or leave the host plan mode |
|
|
333
|
+
| `/goal [objective|clear|edit text|pause|resume]` | View, set, edit, pause, resume, or clear the task goal |
|
|
334
|
+
| `/permission [preset]` | View or switch the host sandbox/approval preset |
|
|
335
|
+
| `/feedback TEXT` | Record feedback about the current session |
|
|
336
|
+
| `/export [local.zip]` | Download the session log ZIP to a new local file |
|
|
331
337
|
| `/older` | Load older history |
|
|
332
338
|
| `/history [text]` | List your own prompts, optionally filtered; Enter jumps to the selected record |
|
|
333
339
|
| `/search <text>` | Search history page by page; choose a match to open its location |
|
|
@@ -342,15 +348,23 @@ Tab completes the leading slash command, extending an ambiguous draft to the sha
|
|
|
342
348
|
| `/think SEQ` | Toggle one loaded thought; `live` toggles the active attempt |
|
|
343
349
|
| `/help`, `/quit` | List every command with its description, or exit |
|
|
344
350
|
|
|
345
|
-
Slash commands work in both pickers and the conversation composer. Typing `/` displays matching commands, and `/help` lists
|
|
351
|
+
Slash commands work in both pickers and the conversation composer. Typing `/` displays matching commands, and `/help` lists commands with their one-line descriptions; PgUp/PgDn changes pages. The `/help`, `/cost`, and `/status` panels remain open until the next command or Esc; Esc leaves the draft in place. `/workspace` and `/workspaces` alias `/ws`; `/session` and `/sessions` alias `/resume`. Names may contain spaces; quotes around the complete target are optional. The unquoted target `all` is reserved for `/resume all`; use `/resume "all"` or an ID to open a session titled `all`. Ambiguous targets require a full ID. Switching a workspace opens its sessions and detaches the old transcript; switching sessions updates the workspace label. Neither operation cancels a remote agent.
|
|
346
352
|
|
|
347
353
|
Type `@` at the end of the draft to search files and directories in the selected session's working directory **on the host**. Use ↑/↓ to select and Tab or Enter to insert; selecting a directory continues completion inside it. Paths with spaces use `@"path with spaces"`. Escape closes the menu and requests cancellation when the agent is running; after closing it, Enter sends the literal draft, including an unmatched path. Lookup failures remain visible and do not submit the draft. Completion operates on the trailing reference, not the cursor position inside existing text.
|
|
348
354
|
|
|
349
355
|
A file reference sends only `@path` in a text block. Harness instructs the model to read the referenced file or list the directory when needed; the TUI does not read local files, upload bytes, or expand contents into the prompt. Referencing an image path does not attach image data. Local attachments, image uploads/previews, and `@` session references are not implemented.
|
|
350
356
|
|
|
351
|
-
|
|
357
|
+
Pending ordinary messages appear inside the composer, with up to two previews. `/queue` opens the full pending-input picker; ↑/↓ selects and Enter, `d`, or the dedicated Delete key removes an item through the host. Esc closes this picker without cancelling the task. A claimed item is no longer removable; the host reports that race instead of resubmitting it. The control stream owns the list, including reconnect replacement and removal when input is claimed; the client does not keep a second submission queue. Questions and approvals take precedence over queue navigation, and their answers never become steering. Slash commands keep their own execution semantics. Queue previews and deletion require the host `session/control` and `session/updateQueue` capabilities.
|
|
352
358
|
|
|
353
|
-
|
|
359
|
+
`/plan`, `/goal`, `/permission`, and `/feedback` invoke the host command registry directly. The loaded host plugins determine availability, accepted arguments, and busy restrictions. Results and errors are displayed locally; an error keeps the input. Feedback is excluded from local input recall. `/export` streams the authenticated session ZIP into a local file (a timestamped filename in the working directory by default); paths with spaces can be quoted. It never overwrites an existing file and removes an incomplete download on failure or cancellation.
|
|
360
|
+
|
|
361
|
+
`/compact` uses the host command endpoint rather than the prompt queue. The host requires an idle agent with no waking queued work; busy, unsupported, and failed commands display their reason and preserve the draft. A progress indicator remains visible while executing; Esc/Ctrl+C cancels the request. Compaction bypasses the ordinary 15-second RPC timeout and is never retried automatically. After an interrupted connection, check the session before retrying because the client cannot determine whether the host finished.
|
|
362
|
+
|
|
363
|
+
Below 62 terminal columns, active streaming reasoning defaults to one folded row. Wider terminals expand active reasoning and fold it on completion. `/think live` toggles the current reasoning; a new response restores the default.
|
|
364
|
+
|
|
365
|
+
User questions show progress, numbered options, and descriptions. Selection lists, pending questions, approvals, and file completions share the composer border with the text input. Pending questions and approvals retain recent conversation history above the composer. The history viewport fits the remaining height, removes extra vertical margins while a dialog is open, and refreshes on explicit scrolling or viewport resizing. The visible option window adapts to terminal height and follows the highlighted choice. With an empty composer, ↑/↓ or 1–9 selects an option and Enter confirms; numbers only select and do not submit. For multi-select questions, Space or 1–9 toggles checkboxes, and Enter confirms the selection. Options beyond nine remain reachable with arrows. Choose Other answer to type numeric free text; ordinary text answers remain supported. Existing drafts keep normal typing, and Escape returns from Other to the options without cancelling the question. All questions are submitted together as structured selected labels and optional custom text; a failed submission preserves the answers for retry. Recognized question and approval events are retained by event ID for the connection, including replay before session selection; only the selected session displays them. Switching pickers does not decline those requests. Unrecognized waterfalls still delegate with `next`. The live host replays pending events after client reconnection; client restart does not preserve unsubmitted answer drafts. Normal TUI shutdown cancels a running turn. A cancelled/failed tool call or a host restart cannot restore the original wait from local UI state; send a new prompt requesting the questions again. Failed submissions retain their input; an interrupted HTTP response can leave delivery uncertain, so check the transcript before manually resending. The client never retries a mutation automatically.
|
|
366
|
+
|
|
367
|
+
The header stays above the scrollable conversation while the composer and status stay below it. The always-visible keyboard legend is removed; `/help` contains the full shortcuts and pickers retain their local navigation hints. The single-line header prioritizes the latest session title (falling back to the ID), with the workspace name after it on wider terminals; host and connection labels are omitted. Without a selected session, it shows the workspace name or All workspaces. A divider sits below the title; `/status` retains the complete session ID. The cancellation acknowledgement stays visible through incoming history until the host reports idle; acceptance does not mean a tool process has already exited.
|
|
354
368
|
|
|
355
369
|
## Live status
|
|
356
370
|
|
|
@@ -364,7 +378,7 @@ The default [Catppuccin Mocha](https://catppuccin.com/palette/) theme distinguis
|
|
|
364
378
|
|
|
365
379
|
Reasoning streams in full while being generated, then folds when its block closes or answer/tool output begins. `/think` opens a newest-first list of reasoning summaries with the preceding loaded user prompt and an entry for the active attempt. Select with ↑/↓ and Enter to jump to the original message and expand it; `/think SEQ` folds or expands that message, and `/think live` controls the active attempt. The list stays open until selection, Esc, or another command. It initially uses loaded history; `Load older reasoning` fetches one earlier page on demand. If a prompt precedes the loaded window, the list says so until that page is loaded. Full reasoning remains available for search.
|
|
366
380
|
|
|
367
|
-
`/model` reads `session/modelCatalog` and offers the provider/model routes and reasoning efforts advertised by the host. Selection calls `session/selectModel` with `{ request: { sessionId, provider, model, reasoningEffort? } }`; omitting effort uses the adapter default. The host applies it to subsequent requests, logs the selection, and also attempts to save it as the deployment default. It does not replace an in-flight request. `modelSelection.next` and `lastUsed` remain the authority for the displayed model; failures retain the previous selection. Provider catalog failures are shown without hiding healthy providers. The header follows the web agent-preset label: `agentPreset` supplies the current ID and `agentPresets/list` supplies names and trust metadata. Built-in system presets display Standard mode, PTC mode, Minimal mode, or Creator mode. Custom presets retain their names; missing roster entries fall back to the ID. The optional roster loads only when needed and is reused for the connection. Plan is a separate feature and does not determine this mode label. Below 62 terminal columns, mode remains available in `/status`
|
|
381
|
+
`/model` reads `session/modelCatalog` and offers the provider/model routes and reasoning efforts advertised by the host. Selection calls `session/selectModel` with `{ request: { sessionId, provider, model, reasoningEffort? } }`; omitting effort uses the adapter default. The host applies it to subsequent requests, logs the selection, and also attempts to save it as the deployment default. It does not replace an in-flight request. `modelSelection.next` and `lastUsed` remain the authority for the displayed model; failures retain the previous selection. Provider catalog failures are shown without hiding healthy providers. The header follows the web agent-preset label: `agentPreset` supplies the current ID and `agentPresets/list` supplies names and trust metadata. Built-in system presets display Standard mode, PTC mode, Minimal mode, or Creator mode. Custom presets retain their names; missing roster entries fall back to the ID. The optional roster loads only when needed and is reused for the connection. Plan is a separate feature and does not determine this mode label. Below 62 terminal columns, mode remains available in `/status` to leave room for the session title.
|
|
368
382
|
|
|
369
383
|
History separates semantic message blocks, prompt/reasoning summaries, view-only fold state, and a row index. Stream frames reuse committed offsets and materialize only the viewport. A per-session LRU holds at most 2,048 committed terminal rows; evicted rows are recreated when revisited. Finished legacy chunks and unused tool-result bodies are released, while the host retains the original log. The host log is the durable tier; the client is a reloadable memory tier. The live tail defaults to soft budgets of 2,000 records or 16 MiB of estimated semantic payload (`--history-records`, `--history-mb`). Eviction targets 75% of the budgets and releases old text, summaries, and layout caches. Switching sessions disposes the previous transcript. Scrolled reading and reasoning navigation protect the loaded window; `/latest` returns to live output and resumes reclamation. Offline history, unfinished streams, and a minimum recent tail are protected, so these limits are not a process RSS cap. First layout, width changes, and an expanded very large block still require wrapping that content. `npm run bench:history` measures local stream/layout cost at 500, 2,000, and 10,000 messages without model or network time.
|
|
370
384
|
|
|
@@ -398,7 +412,7 @@ This repository publishes one public package, `@itookit/dsht`, from the `mushuan
|
|
|
398
412
|
|
|
399
413
|
| Field | Value |
|
|
400
414
|
| --- | --- |
|
|
401
|
-
| Name and version | `@itookit/dsht` `0.
|
|
415
|
+
| Name and version | `@itookit/dsht` `0.3.0` |
|
|
402
416
|
| Executable | `dsht`, or `npx @itookit/dsht` without installing |
|
|
403
417
|
| Library entries | `@itookit/dsht` and `@itookit/dsht/auth` |
|
|
404
418
|
| Author | lizlok@gmail.com |
|
|
@@ -440,4 +454,4 @@ Tests use isolated HTTP/WebSocket hosts, drive the real Ink picker and composer,
|
|
|
440
454
|
|
|
441
455
|
Typing reuses history projection and wrapping until the transcript revision or terminal width changes; host updates and older pages invalidate that reuse. `bench:input` measures local input-to-render work with 20 and 500 synthetic messages, 30 measured keystrokes after warmup, and history projection read counts. It excludes network/model time and is a diagnostic, not a machine-independent latency threshold.
|
|
442
456
|
|
|
443
|
-
The interface presents plain text, reasoning, tool calls, and tool results. Rich plugin cards, file upload, subagent navigation, and
|
|
457
|
+
The interface presents plain text, reasoning, tool calls, and tool results. Rich plugin cards, file upload, subagent navigation, and editing queued message text are not implemented. Pending-message deletion is supported through `/queue`. Reconnect uses bounded exponential backoff with jitter and replaces snapshots; list commands fail directly instead of retrying. Updating pre-stable host APIs requires updating the local wire adapter and tests.
|
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
> **dsht — 只要有终端,就能随时控制 DeepSeek Harness。**
|
|
8
8
|
|
|
@@ -81,7 +81,7 @@ DeepSeek Harness 往往运行在性能更强、环境更完整的开发工作站
|
|
|
81
81
|
|
|
82
82
|
- 查看正在运行的任务和实时输出;
|
|
83
83
|
- 阅读助手回复、思考内容和工具执行状态;
|
|
84
|
-
- 发送新的 prompt
|
|
84
|
+
- 发送新的 prompt,任务运行中自动作为转向输入;
|
|
85
85
|
- `/allow` 或 `/deny` 审批操作;
|
|
86
86
|
- 回答 Harness 提出的问题;
|
|
87
87
|
- `/cancel` 停止当前轮次;
|
|
@@ -287,9 +287,9 @@ JSON 输出格式为 `{ "items": [...] }`;省略 `--json` 则输出制表符
|
|
|
287
287
|
|
|
288
288
|
## 对话操作
|
|
289
289
|
|
|
290
|
-
Enter
|
|
290
|
+
Enter 提交消息:代理处于 Working 时自动作为下一个 step 的转向输入;空闲时开启新一轮。转向输入等待当前步骤及其工具执行完成,不会中断正在运行的工具。输入框非空时,Ctrl+C 先清空输入;否则所选会话运行中时请求取消,只有空闲时才退出,连续按键会复用尚未完成的取消请求。取消会等待正在提交的消息完成接收,失败时保留客户端。聊天界面中 Esc 会发送取消请求,不受本地空闲状态判断限制。任务运行中时,Esc 关闭文件或历史/搜索菜单的同时请求取消;空闲菜单仅关闭。正在执行的历史/搜索/费用加载、服务端命令和导出请求优先被取消。Page Up/Down 滚动当前对话;`/older` 加载更早记录。所有退出路径(包括 `/quit` 和 SIGTERM)都会在关闭连接前停止所选任务,因此退出不会留下仍在运行的代理;会话空闲时不发送取消。取消当前任务会保留排队消息。
|
|
291
291
|
|
|
292
|
-
`/copy`、Ctrl+S 或普通聊天界面的鼠标左键单击冻结画面并关闭鼠标事件捕获,便于使用终端原生选择复制。Esc、Ctrl+S 或 Ctrl+C
|
|
292
|
+
`/copy`、Ctrl+S 或普通聊天界面的鼠标左键单击冻结画面并关闭鼠标事件捕获,便于使用终端原生选择复制。Esc、Ctrl+S 或 Ctrl+C 退出复制模式并显示最新输出,退出复制模式不会取消代理。对话框和选择器暂停背景标题、对话和状态的自动更新。对话历史保留在输入框上方,鼠标滚轮及 PgUp/PgDn 可滚动历史,不会移动当前选项;帮助面板中的 PgUp/PgDn 用于帮助翻页。对话框中的鼠标单击不会进入复制模式,Ctrl+S 可冻结整个画面并释放鼠标捕获以进行原生选择。回看旧历史时 Working 计时显示也暂停。帮助/状态/费用面板不再定时消失。后台接收与内存回收继续运行,调整窗口大小仍可能重绘。
|
|
293
293
|
|
|
294
294
|
鼠标滚轮和 Page Up/Down 滚动对话;滚到顶部自动加载更早的一页。查看旧记录时,新输出保留阅读位置;向下滚动即可恢复跟随最新输出。TUI 挂载时启用鼠标报告,退出时关闭,需要终端支持 SGR 鼠标报告。加载历史或搜索期间,Esc 或 Ctrl+C 优先取消本地操作,不中断远程任务。
|
|
295
295
|
|
|
@@ -301,7 +301,7 @@ Enter 提交消息。输入框非空时,Ctrl+C 先清空输入;否则所选
|
|
|
301
301
|
|
|
302
302
|
每条 User 消息之后,只在第一段助手正文或思考前显示 Assistant 标题;后续消息及流式输出沿用分组,工具结果和 Context 消息不重置分组。当前加载的历史窗口从自身起点建立可见分组,消息序号、工具状态、搜索和思考展开仍各自保留。
|
|
303
303
|
|
|
304
|
-
鼠标复制时,先单击进入复制模式,待画面冻结后再拖动选择。松开鼠标不会恢复刷新,需按 Esc、Ctrl+S 或 Ctrl+C。终端原生 Shift+拖选可能不向应用发送鼠标事件,此时请先按 Ctrl+S
|
|
304
|
+
鼠标复制时,先单击进入复制模式,待画面冻结后再拖动选择。松开鼠标不会恢复刷新,需按 Esc、Ctrl+S 或 Ctrl+C。终端原生 Shift+拖选可能不向应用发送鼠标事件,此时请先按 Ctrl+S。对话框中可按 Ctrl+S 冻结整个画面并释放鼠标捕获,再进行原生选择。
|
|
305
305
|
|
|
306
306
|
Tab 补全开头的 slash 命令,多个候选时补到公共前缀。单行输入框支持 Readline 风格编辑。单词以空白分隔;光标移动和逐字符删除保持完整的 Unicode 组合字符。粘贴的多行文本会以空格连接成一行。空输入时 Ctrl+D 不退出;输入非空时 Ctrl+C 先清空输入,然后才停止或退出。未处理的修饰键快捷键不会将控制字符插入消息。终端退格键的 BS 和 DEL 编码均向后删除;独立 Delete 键(CSI 3~)向前删除。
|
|
307
307
|
|
|
@@ -326,8 +326,14 @@ Tab 补全开头的 slash 命令,多个候选时补到公共前缀。单行输
|
|
|
326
326
|
| `/resume all` | 显示所有工作区的会话 |
|
|
327
327
|
| `/model [provider model [effort]]` | 选择模型及其思考强度,也可直接提交精确路由 ID |
|
|
328
328
|
| `/new` | 在所选工作区创建会话 |
|
|
329
|
+
| `/compact` | 会话空闲时压缩旧历史,显示服务端结果 |
|
|
329
330
|
| `/cancel` | 取消当前轮次,保留待处理队列 |
|
|
330
|
-
| `/
|
|
331
|
+
| `/queue` | 查看待处理输入;方向键选择,Enter/d/Delete 删除 |
|
|
332
|
+
| `/plan [off|message]` | 进入或退出服务端计划模式 |
|
|
333
|
+
| `/goal [objective|clear|edit text|pause|resume]` | 查看、设置、编辑、暂停、恢复或清除目标 |
|
|
334
|
+
| `/permission [preset]` | 查看或切换服务端沙箱与审批预设 |
|
|
335
|
+
| `/feedback TEXT` | 记录当前会话反馈 |
|
|
336
|
+
| `/export [local.zip]` | 下载会话日志 ZIP 到新的本地文件 |
|
|
331
337
|
| `/older` | 加载更早的历史 |
|
|
332
338
|
| `/history [text]` | 列出自己的提示词并可选筛选;Enter 跳到所选记录 |
|
|
333
339
|
| `/search <text>` | 逐页搜索历史,选择命中项后打开其位置 |
|
|
@@ -342,15 +348,23 @@ Tab 补全开头的 slash 命令,多个候选时补到公共前缀。单行输
|
|
|
342
348
|
| `/think SEQ` | 切换一条已加载思考的展开状态;`live` 表示当前尝试 |
|
|
343
349
|
| `/help`, `/quit` | 列出每条命令及其说明,或退出 |
|
|
344
350
|
|
|
345
|
-
Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示匹配命令,`/help`
|
|
351
|
+
Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示匹配命令,`/help` 分页列出命令及其单行说明,PgUp/PgDn 翻页。`/help`、`/cost`、`/status` 面板保持打开,直到下一条命令或 Esc;Esc 保留输入内容。`/workspace`、`/workspaces` 是 `/ws` 的别名;`/session`、`/sessions` 是 `/resume` 的别名。名称可以包含空格,完整目标两侧的引号可选。不带引号的目标 `all` 保留给 `/resume all`;打开标题为 `all` 的会话时,使用 `/resume "all"` 或其 ID。目标有歧义时必须提供完整 ID。切换工作区会打开其会话列表并解除旧对话订阅;切换会话会同步工作区标签。两种操作均不会取消远程代理。
|
|
346
352
|
|
|
347
353
|
在输入末尾键入 `@`,可搜索所选会话**在服务端**工作目录中的文件和目录。使用 ↑/↓ 选择,Tab 或 Enter 插入;选择目录后继续补全其内部路径。带空格的路径使用 `@"path with spaces"`。Esc 关闭菜单,任务运行中时同时请求取消;关闭后 Enter 发送原样输入,包括未匹配到的路径。搜索失败时显示错误,不提交输入。补全针对输入末尾的引用,不跟踪已有文本内部的光标位置。
|
|
348
354
|
|
|
349
355
|
文件引用仅在文本块中发送 `@path`。Harness 提示模型按需读取文件或列出目录;TUI 不读取本地文件、不上传字节,也不将文件内容展开进提示词。引用图片路径不会附带图片数据。尚未实现本地附件、图片上传/预览及 `@` 会话引用。
|
|
350
356
|
|
|
351
|
-
|
|
357
|
+
待处理的普通消息显示在输入框内,最多预览两条。`/queue` 打开完整的待处理输入列表,↑/↓ 选择,Enter、`d` 或独立 Delete 键通过服务端删除。Esc 仅关闭此列表,不取消任务。消息已被领取后无法删除,服务端会提示该竞争情况,客户端不会重新发送。控制流负责列表、重连替换及消息领取后的移除,客户端不维护第二份发送队列。问题和审批优先于队列导航,其回答不会成为转向输入;slash 命令仍按各自语义执行。队列预览和删除需要服务端提供 `session/control` 与 `session/updateQueue`。
|
|
352
358
|
|
|
353
|
-
|
|
359
|
+
`/plan`、`/goal`、`/permission` 和 `/feedback` 直接调用服务端命令注册表。命令可用性、参数和忙碌限制由服务端加载的插件决定。结果和错误显示在本地,失败时保留输入;反馈内容不进入本地输入回填历史。`/export` 将经过认证的会话 ZIP 流式写入本地文件,默认在当前工作目录使用带时间戳的文件名;含空格路径可加引号。已有文件不会被覆盖,失败或取消时清理未完成的下载文件。
|
|
360
|
+
|
|
361
|
+
`/compact` 通过服务端命令接口执行,不作为普通消息排队。服务端要求代理空闲且没有即将执行的排队任务;忙碌、未提供此命令或执行失败时显示原因并保留输入。执行期间显示进度提示,Esc/Ctrl+C 取消请求;压缩请求不使用普通 RPC 的 15 秒超时,也不会自动重试。中断连接后不能仅凭客户端判断服务端是否已完成,请先检查会话状态。
|
|
362
|
+
|
|
363
|
+
终端少于 62 列时,最新流式思考也默认折叠为一行;宽屏保持流式展开、完成后折叠。`/think live` 可手动展开或折叠当前思考,新回复恢复默认行为。
|
|
364
|
+
|
|
365
|
+
用户问题显示题目进度、编号选项和说明。选择列表、待答问题、审批和文件补全与文本输入共用一个输入框边框。待答问题和审批会在输入框上方保留最近对话历史,历史视口使用剩余高度;对话框打开时减少上下留白,并在手动滚动或视口高度变化时刷新;可见选项数量按终端高度调整,并跟随当前高亮项滚动。输入框为空时,↑/↓ 或 1–9 定位选项,Enter 确认;数字键只选择、不提交。多选题用空格或 1–9 勾选/取消勾选,Enter 确认,超过九个选项仍可通过方向键访问。选择 Other answer 后可输入纯数字自由文本,也保留普通文本回答。已有草稿时按正常文字输入处理,Esc 从 Other 返回选项而不取消提问。全部题目回答完成后,一次提交结构化选项标签及可选自定义文本;失败时保留答案以便重试。已识别的提问和审批事件在本次连接中按事件 ID 保留,包括早于会话选择到达的重放事件;只展示当前会话对应的请求。切换列表不会退回这些请求,不识别的 waterfall 仍通过 `next` 委托后续处理。存活服务端在客户端重连后重发待答事件;客户端重启不保留尚未提交的回答草稿。正常退出 TUI 会取消正在运行的轮次。调用已取消/失败或服务端已重启时,无法靠本地 UI 状态恢复原等待,需要发送新提示词要求重新提问。提交失败时保留输入;HTTP 响应中断可能导致投递状态不确定,手动重发前应检查会话记录。客户端不会自动重试修改请求。
|
|
366
|
+
|
|
367
|
+
标题固定在可滚动对话区域上方,输入框和状态栏保留在下方。底部不再常驻快捷键说明,完整快捷键放在 `/help`,选择器只显示当前需要的导航提示。顶部单行优先显示最新会话标题(无标题时回退到 ID),宽屏时在其后附上工作区名称,不再显示主机地址和连接状态。未选择会话时显示工作区名称或 All workspaces,下方为分隔线;`/status` 保留完整会话 ID。取消回执在后续历史消息到达时保持可见,直到服务端报告空闲;接受取消不表示工具进程已经退出。
|
|
354
368
|
|
|
355
369
|
## 实时状态
|
|
356
370
|
|
|
@@ -364,7 +378,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
364
378
|
|
|
365
379
|
思考生成时完整流式显示,思考块结束或正文/工具输出开始后自动折叠。`/think` 按从新到旧列出思考摘要、前一条已加载的用户 prompt,并包含当前尝试。↑/↓ 选择、Enter 跳到原消息并展开;`/think SEQ` 可切换该消息的折叠状态,`/think live` 控制当前尝试。列表在选择、Esc 或其他命令时关闭,不自动超时。打开列表只使用已加载记录,选择 `Load older reasoning` 才读取一页更早历史;prompt 在已加载窗口之前时明确提示,加载对应页面后补全。完整思考仍可被搜索。
|
|
366
380
|
|
|
367
|
-
`/model` 读取 `session/modelCatalog`,展示服务端公布的 provider/model 路由及思考强度。选择通过 `session/selectModel` 提交 `{ request: { sessionId, provider, model, reasoningEffort? } }`,省略强度时使用适配器默认值。服务端将选择用于后续请求、记录选择事件,并尝试保存为部署默认值;不会替换正在执行的请求。展示模型继续以 `modelSelection.next` 和 `lastUsed` 为准,调用失败保留原选择。部分 provider 目录失败会单独提示,不隐藏正常 provider。标题与网页的 Agent preset 标签一致:`agentPreset` 提供当前 ID,`agentPresets/list` 提供名称和信任来源。内置系统预设显示 Standard mode、PTC mode、Minimal mode、Creator mode;自定义预设保留其名称,目录缺失时回退显示 ID。可选目录按需读取并在本次连接中复用。Plan 是独立功能,不决定这里的模式名称。终端少于 62 列时,mode 可在 `/status`
|
|
381
|
+
`/model` 读取 `session/modelCatalog`,展示服务端公布的 provider/model 路由及思考强度。选择通过 `session/selectModel` 提交 `{ request: { sessionId, provider, model, reasoningEffort? } }`,省略强度时使用适配器默认值。服务端将选择用于后续请求、记录选择事件,并尝试保存为部署默认值;不会替换正在执行的请求。展示模型继续以 `modelSelection.next` 和 `lastUsed` 为准,调用失败保留原选择。部分 provider 目录失败会单独提示,不隐藏正常 provider。标题与网页的 Agent preset 标签一致:`agentPreset` 提供当前 ID,`agentPresets/list` 提供名称和信任来源。内置系统预设显示 Standard mode、PTC mode、Minimal mode、Creator mode;自定义预设保留其名称,目录缺失时回退显示 ID。可选目录按需读取并在本次连接中复用。Plan 是独立功能,不决定这里的模式名称。终端少于 62 列时,mode 可在 `/status` 查看,为会话标题留出空间。
|
|
368
382
|
|
|
369
383
|
历史分为语义消息块、prompt/思考摘要、独立折叠状态和行数索引。流式更新复用已提交历史的位置索引,只生成当前可见区域。每个会话的 LRU 最多保留 2,048 行已提交终端内容,移出缓存的行在回看时重建。已结束的旧版流式分片和不用展示的工具结果正文会释放,原始日志由服务端保存。服务端日志作为持久层,客户端作为可重载的内存层。实时历史默认以 2,000 条记录或 16 MiB 语义数据估算量为软限制(`--history-records`、`--history-mb`),触发回收后以限制的 75% 为目标,释放旧正文、摘要和排版缓存。切换会话会释放上一会话的 transcript。回看和思考导航期间保护已加载窗口;`/latest` 返回实时输出并恢复回收。离线历史、未结束流和最小近期尾部受保护,因此这些参数不是进程 RSS 硬上限。首次排版、改变终端宽度及展开特别大的单个内容块,仍需要处理对应全文。`npm run bench:history` 测量 500、2,000、10,000 条消息下的本地流式排版耗时,不含网络和模型时间。
|
|
370
384
|
|
|
@@ -398,7 +412,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
398
412
|
|
|
399
413
|
| 字段 | 值 |
|
|
400
414
|
| --- | --- |
|
|
401
|
-
| 名称与版本 | `@itookit/dsht` `0.
|
|
415
|
+
| 名称与版本 | `@itookit/dsht` `0.3.0` |
|
|
402
416
|
| 可执行命令 | `dsht`,不安装时用 `npx @itookit/dsht` |
|
|
403
417
|
| 库入口 | `@itookit/dsht` 和 `@itookit/dsht/auth` |
|
|
404
418
|
| 作者 | lizlok\@gmail.com |
|
|
@@ -440,4 +454,4 @@ node dist/cli.js --help
|
|
|
440
454
|
|
|
441
455
|
输入期间复用历史投影和换行结果,直到对话版本或终端宽度变化;服务端更新和历史翻页会使缓存失效。`bench:input` 使用 20 条和 500 条合成消息,在预热后测量 30 次按键的本地输入至渲染耗时及历史投影读取次数。它排除网络/模型耗时,仅供诊断,不作为跨机器的延迟阈值。
|
|
442
456
|
|
|
443
|
-
|
|
457
|
+
界面显示纯文本、思考内容、工具调用和工具结果。尚未实现富插件卡片、文件上传、子代理导航和队列消息文本编辑;可通过 `/queue` 删除待处理消息。重连采用有上限的指数退避及抖动,并替换快照;列表命令直接报告失败而不重试。服务端的非稳定 API 更新后,需要同步本地报文适配和测试。
|
package/dist/app.js
CHANGED
|
@@ -27,8 +27,14 @@ export const COMMAND_HINTS = [
|
|
|
27
27
|
{ command: '/search', usage: 'text', description: 'Search history page by page and open a match' },
|
|
28
28
|
{ command: '/ssearch', usage: 'text', description: 'Search sessions in the current workspace' },
|
|
29
29
|
{ command: '/wsearch', usage: 'text', description: 'Search sessions across all workspaces' },
|
|
30
|
+
{ command: '/compact', description: 'Compact older history while the session is idle' },
|
|
30
31
|
{ command: '/cancel', description: 'Cancel the active turn' },
|
|
31
|
-
{ command: '/
|
|
32
|
+
{ command: '/queue', description: 'View and remove pending input' },
|
|
33
|
+
{ command: '/plan', usage: '[off|message]', description: 'Enter or leave host plan mode' },
|
|
34
|
+
{ command: '/goal', usage: '[action|objective]', description: 'View or manage the host task goal' },
|
|
35
|
+
{ command: '/permission', usage: '[preset]', description: 'View or switch the host permission preset' },
|
|
36
|
+
{ command: '/feedback', usage: 'text', description: 'Record feedback about the session' },
|
|
37
|
+
{ command: '/export', usage: '[local.zip]', description: 'Save the session log ZIP to a new local file' },
|
|
32
38
|
{ command: '/allow', description: 'Approve the pending request once' },
|
|
33
39
|
{ command: '/deny', description: 'Reject the pending request' },
|
|
34
40
|
{ command: '/status', description: 'Show full session status details' },
|
|
@@ -60,7 +66,7 @@ const CopyMode = createContext(false);
|
|
|
60
66
|
const Frozen = memo(function Frozen({ children }) {
|
|
61
67
|
return _jsx(_Fragment, { children: children });
|
|
62
68
|
}, (previous, next) => previous.frozen && next.frozen && previous.identity === next.identity);
|
|
63
|
-
function Picker({ choices, enabled, canSelect, pageSize = 12 }) {
|
|
69
|
+
function Picker({ choices, enabled, canSelect, pageSize = 12, hint }) {
|
|
64
70
|
const theme = useTheme();
|
|
65
71
|
const copyMode = useContext(CopyMode);
|
|
66
72
|
const [selected, setSelected] = useState(0);
|
|
@@ -85,7 +91,7 @@ function Picker({ choices, enabled, canSelect, pageSize = 12 }) {
|
|
|
85
91
|
choices[current]?.action();
|
|
86
92
|
}, { isActive: enabled && !copyMode });
|
|
87
93
|
const start = Math.max(0, current - Math.max(0, pageSize - 1));
|
|
88
|
-
return _jsxs(Box, { flexDirection: "column", children: [choices.slice(start, start + pageSize).map((choice, index) => _jsxs(Text, { color: start + index === current ? theme.accent : undefined, children: [start + index === current ? '❯ ' : ' ', safeText(choice.label)] }, choice.key)),
|
|
94
|
+
return _jsxs(Box, { flexDirection: "column", children: [choices.slice(start, start + pageSize).map((choice, index) => _jsxs(Text, { color: start + index === current ? theme.accent : undefined, children: [start + index === current ? '❯ ' : ' ', safeText(choice.label)] }, choice.key)), _jsx(Text, { dimColor: true, children: hint ?? `↑ ↓ select · Enter open${choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : ''} · Ctrl+C stop / exit` })] });
|
|
89
95
|
}
|
|
90
96
|
/** The caller owns starting and stopping the controller around the Ink render lifetime. */
|
|
91
97
|
export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = mocha }) {
|
|
@@ -120,7 +126,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
120
126
|
setCursor(value.length);
|
|
121
127
|
};
|
|
122
128
|
const [historyWindow, setHistoryWindow] = useState();
|
|
123
|
-
const [historyLoading, setHistoryLoading] = useState(
|
|
129
|
+
const [historyLoading, setHistoryLoading] = useState();
|
|
124
130
|
const [historyMatches, setHistoryMatches] = useState();
|
|
125
131
|
const displayTranscript = historyWindow ?? state.transcript;
|
|
126
132
|
const displayRef = useRef(displayTranscript);
|
|
@@ -143,6 +149,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
143
149
|
const [removal, setRemoval] = useState();
|
|
144
150
|
const [models, setModels] = useState();
|
|
145
151
|
const [thoughtList, setThoughtList] = useState(false);
|
|
152
|
+
const [queueOpen, setQueueOpen] = useState(false);
|
|
146
153
|
const [historyQuery, setHistoryQuery] = useState();
|
|
147
154
|
const [contentSearch, setContentSearch] = useState(false);
|
|
148
155
|
const [searchResults, setSearchResults] = useState();
|
|
@@ -156,6 +163,10 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
156
163
|
useEffect(() => { setLiveReasoning('row'); }, [state.transcript, state.transcript.liveAttemptKey]);
|
|
157
164
|
const [notice, setNotice] = useState();
|
|
158
165
|
const [help, setHelp] = useState(false);
|
|
166
|
+
const [helpPage, setHelpPage] = useState(0);
|
|
167
|
+
const helpPageSize = Math.max(1, (stdout.rows ?? 30) - 12);
|
|
168
|
+
const helpPages = Math.ceil(COMMAND_HINTS.length / helpPageSize);
|
|
169
|
+
const currentHelpPage = Math.min(helpPage, helpPages - 1);
|
|
159
170
|
const [answers, setAnswers] = useState({});
|
|
160
171
|
const [optionState, setOptionState] = useState();
|
|
161
172
|
const [referenceIndex, setReferenceIndex] = useState(0);
|
|
@@ -171,11 +182,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
171
182
|
return () => clearTimeout(timer);
|
|
172
183
|
}, [notice, panelLifetimeMs, copyMode]);
|
|
173
184
|
const pending = state.pending[0];
|
|
185
|
+
const queued = controller.telemetry.pending(state.sessionId).filter(item => item.placement !== 'context');
|
|
186
|
+
useEffect(() => { setQueueOpen(false); }, [state.sessionId, pending?.eventId]);
|
|
174
187
|
const token = state.screen === 'chat' && state.online && !state.busy && !pending
|
|
175
|
-
&&
|
|
188
|
+
&& !input.startsWith('/') && dismissedReference !== input && cursor === input.length
|
|
176
189
|
? activeReference(input) : undefined;
|
|
177
190
|
const referenceOpen = token !== undefined;
|
|
178
|
-
const dialogOpen = !!(removal || models || thoughtList || historyQuery !== undefined || searchResults || costExpanded || statusExpanded || help || pending || referenceOpen || state.screen !== 'chat');
|
|
191
|
+
const dialogOpen = !!(queueOpen || removal || models || thoughtList || historyQuery !== undefined || searchResults || costExpanded || statusExpanded || help || pending || referenceOpen || state.screen !== 'chat');
|
|
179
192
|
const displayPaused = copyMode || dialogOpen;
|
|
180
193
|
const matches = referenceOpen && lookup?.draft === input && lookup.sessionId === state.sessionId ? lookup : undefined;
|
|
181
194
|
useEffect(() => {
|
|
@@ -310,6 +323,18 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
310
323
|
return;
|
|
311
324
|
}
|
|
312
325
|
}
|
|
326
|
+
if (help && (key.pageUp || key.pageDown)) {
|
|
327
|
+
setHelpPage(Math.max(0, Math.min(helpPages - 1, currentHelpPage + (key.pageUp ? -1 : 1))));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (key.pageUp || key.pageDown) {
|
|
331
|
+
scrollHistory(key.pageUp ? 10 : -10);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (key.escape && queueOpen) {
|
|
335
|
+
setQueueOpen(false);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
313
338
|
if (key.escape && removal) {
|
|
314
339
|
setRemoval(undefined);
|
|
315
340
|
return;
|
|
@@ -364,7 +389,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
364
389
|
const recallPrevious = key.upArrow || key.ctrl && _value === 'p';
|
|
365
390
|
const recallNext = key.downArrow || key.ctrl && _value === 'n';
|
|
366
391
|
if ((recallPrevious || recallNext) && state.online && !controller.state.busy && !pending
|
|
367
|
-
&& !removal && !models && !thoughtList && historyQuery === undefined && !searchResults && !help && !costExpanded && !statusExpanded
|
|
392
|
+
&& !queueOpen && !removal && !models && !thoughtList && historyQuery === undefined && !searchResults && !help && !costExpanded && !statusExpanded
|
|
368
393
|
&& (state.screen === 'chat' || draft.current !== '' || key.ctrl)) {
|
|
369
394
|
setInput(inputHistory.current.move(recallPrevious ? -1 : 1, draft.current), true);
|
|
370
395
|
return;
|
|
@@ -385,10 +410,6 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
385
410
|
if (key.escape && state.screen === 'chat') {
|
|
386
411
|
void controller.interrupt(true);
|
|
387
412
|
}
|
|
388
|
-
if (key.pageUp)
|
|
389
|
-
scrollHistory(10);
|
|
390
|
-
if (key.pageDown)
|
|
391
|
-
scrollHistory(-10);
|
|
392
413
|
});
|
|
393
414
|
const submit = async (raw) => {
|
|
394
415
|
if (referenceOpen) {
|
|
@@ -400,7 +421,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
400
421
|
const value = raw.trim();
|
|
401
422
|
if (!value)
|
|
402
423
|
return;
|
|
403
|
-
if (!pending)
|
|
424
|
+
if (!pending && !/^\/feedback(?:\s|$)/.test(value))
|
|
404
425
|
inputHistory.current.record(value);
|
|
405
426
|
if (value === '/copy') {
|
|
406
427
|
setInput('');
|
|
@@ -408,6 +429,8 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
408
429
|
return;
|
|
409
430
|
}
|
|
410
431
|
setRemoval(undefined);
|
|
432
|
+
if (value !== '/queue')
|
|
433
|
+
setQueueOpen(false);
|
|
411
434
|
// Each panel belongs to the command that opened it, so any other command closes it.
|
|
412
435
|
if (!/^\/model(?: |$)/.test(value))
|
|
413
436
|
setModels(undefined);
|
|
@@ -437,6 +460,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
437
460
|
}
|
|
438
461
|
if (value === '/help') {
|
|
439
462
|
setHelp(value => !value);
|
|
463
|
+
setHelpPage(0);
|
|
440
464
|
setInput('');
|
|
441
465
|
return;
|
|
442
466
|
}
|
|
@@ -484,6 +508,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
484
508
|
setModels(undefined);
|
|
485
509
|
}
|
|
486
510
|
}
|
|
511
|
+
else if (value === '/queue') {
|
|
512
|
+
if (state.screen !== 'chat')
|
|
513
|
+
throw new Error('Select a session first');
|
|
514
|
+
if (pending)
|
|
515
|
+
throw new Error('Answer the pending question or approval first');
|
|
516
|
+
setQueueOpen(true);
|
|
517
|
+
}
|
|
487
518
|
else if (value === '/new')
|
|
488
519
|
await controller.createSession();
|
|
489
520
|
else if (value === '/history' || value.startsWith('/history ')) {
|
|
@@ -544,14 +575,34 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
544
575
|
await controller.older(undefined, displayTranscript);
|
|
545
576
|
setScroll(value => value + 10);
|
|
546
577
|
}
|
|
578
|
+
else if (/^\/compact(?: |$)/.test(value)) {
|
|
579
|
+
if (state.screen !== 'chat')
|
|
580
|
+
throw new Error('Select a session first');
|
|
581
|
+
if (value !== '/compact')
|
|
582
|
+
throw new Error('Use /compact (no arguments)');
|
|
583
|
+
setNotice(undefined);
|
|
584
|
+
await historyOperation(async (signal) => { setNotice(await controller.command('/compact', signal)); }, 'Compacting history…');
|
|
585
|
+
}
|
|
547
586
|
else if (value === '/cancel')
|
|
548
587
|
await controller.cancelTurn();
|
|
549
588
|
else if (value === '/allow')
|
|
550
589
|
await controller.approve(true);
|
|
551
590
|
else if (value === '/deny')
|
|
552
591
|
await controller.approve(false);
|
|
553
|
-
else if (
|
|
554
|
-
|
|
592
|
+
else if (/^\/(?:plan|goal|permission|feedback)(?:\s|$)/.test(value)) {
|
|
593
|
+
if (state.screen !== 'chat')
|
|
594
|
+
throw new Error('Select a session first');
|
|
595
|
+
if (pending)
|
|
596
|
+
throw new Error('Answer the pending question or approval first');
|
|
597
|
+
setNotice(undefined);
|
|
598
|
+
await historyOperation(async (signal) => { setNotice(await controller.command(value, signal)); }, 'Running command…');
|
|
599
|
+
}
|
|
600
|
+
else if (/^\/export(?:\s|$)/.test(value)) {
|
|
601
|
+
if (state.screen !== 'chat')
|
|
602
|
+
throw new Error('Select a session first');
|
|
603
|
+
const destination = value.slice(7).trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
604
|
+
await historyOperation(async (signal) => { setNotice(`Saved session log: ${await controller.exportLog(destination || undefined, signal)}`); }, 'Exporting session log…');
|
|
605
|
+
}
|
|
555
606
|
else if (question)
|
|
556
607
|
await answerQuestion(question.multiSelect === true ? choiceState.selected : [], value);
|
|
557
608
|
else if (pending)
|
|
@@ -588,7 +639,8 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
588
639
|
const layout = useMemo(() => historyLayout(displayTranscript, width, reasoning, reasoningOverrides, liveReasoning), [displayTranscript, displayTranscript.version, width, reasoning, reasoningOverrides, liveReasoning]);
|
|
589
640
|
const { length, first } = layout;
|
|
590
641
|
const statusNotice = !['Connected', 'Idle', 'Running…', 'Responding…'].includes(state.status);
|
|
591
|
-
const
|
|
642
|
+
const showHistoryHint = dialogOpen || displayTranscript.hasMore || !!historyWindow;
|
|
643
|
+
const pageSize = Math.max(1, conversationRows - (showHistoryHint ? 1 : 0));
|
|
592
644
|
const previousView = useRef({ transcript: displayTranscript, session: state.transcript, count: length, first, folds: reasoningOverrides, liveReasoning });
|
|
593
645
|
const previous = previousView.current;
|
|
594
646
|
const prepended = previous.first !== undefined && first !== undefined && first < previous.first;
|
|
@@ -619,7 +671,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
619
671
|
const mounted = useRef(true);
|
|
620
672
|
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
621
673
|
function scrollHistory(delta) {
|
|
622
|
-
if (
|
|
674
|
+
if (copyMode || state.screen !== 'chat')
|
|
623
675
|
return;
|
|
624
676
|
const intent = ++scrollIntent.current;
|
|
625
677
|
const next = Math.max(0, Math.min(maxScroll, scrollPosition.current + delta));
|
|
@@ -635,17 +687,17 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
635
687
|
}
|
|
636
688
|
}).finally(() => { loadingPage.current = false; });
|
|
637
689
|
}
|
|
638
|
-
async function historyOperation(operation) {
|
|
690
|
+
async function historyOperation(operation, label = 'Loading history…') {
|
|
639
691
|
const abort = new AbortController();
|
|
640
692
|
historyAbort.current = abort;
|
|
641
|
-
setHistoryLoading(
|
|
693
|
+
setHistoryLoading(label);
|
|
642
694
|
try {
|
|
643
695
|
await operation(abort.signal);
|
|
644
696
|
}
|
|
645
697
|
finally {
|
|
646
698
|
if (historyAbort.current === abort) {
|
|
647
699
|
historyAbort.current = undefined;
|
|
648
|
-
setHistoryLoading(
|
|
700
|
+
setHistoryLoading(undefined);
|
|
649
701
|
}
|
|
650
702
|
}
|
|
651
703
|
}
|
|
@@ -685,9 +737,11 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
685
737
|
historyAbort.current = undefined;
|
|
686
738
|
}
|
|
687
739
|
}
|
|
688
|
-
useMouseWheel(direction => scrollHistory(direction * 3), !
|
|
689
|
-
|
|
690
|
-
const
|
|
740
|
+
useMouseWheel(direction => scrollHistory(direction * 3), !copyMode && state.screen === 'chat', () => { if (!dialogOpen)
|
|
741
|
+
setCopyMode(true); });
|
|
742
|
+
const trailingGap = dialogOpen && length > 0 && layout.viewport(length - 1, length)[0]?.text === '' ? 1 : 0;
|
|
743
|
+
const end = Math.max(pageSize, length - position - trailingGap);
|
|
744
|
+
const visible = useMemo(() => layout.viewport(Math.max(0, end - pageSize), end), [layout, end, pageSize]);
|
|
691
745
|
const liveThought = thoughtList && !historyWindow ? state.transcript.liveParts(width).find(part => part.kind === 'reasoning') : undefined;
|
|
692
746
|
const thoughtEntries = thoughtList ? displayTranscript.thoughts : undefined;
|
|
693
747
|
const thoughtChoices = useMemo(() => [...(thoughtEntries ?? [])].reverse().map(entry => ({
|
|
@@ -703,44 +757,49 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
703
757
|
], [liveThought, thoughtChoices, displayTranscript, displayTranscript.hasMore, width, pageSize, reasoningOverrides]);
|
|
704
758
|
const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
|
|
705
759
|
const workspaceName = workspace ? string(workspace.title) || string(workspace.path) : state.workspaceId;
|
|
706
|
-
const headerTitle =
|
|
707
|
-
.filter(Boolean).join(' · ')
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
key:
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
760
|
+
const headerTitle = controller.sessionName
|
|
761
|
+
? [controller.sessionName, width >= 60 ? workspaceName : undefined].filter(Boolean).join(' · ')
|
|
762
|
+
: workspaceName || 'All workspaces';
|
|
763
|
+
return _jsx(ThemeContext.Provider, { value: theme, children: _jsx(CopyMode.Provider, { value: copyMode, children: _jsx(Frozen, { frozen: copyMode, identity: `${width}:${stdout.rows}`, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, height: Math.max(1, (stdout.rows ?? 30) - 1), overflowY: "hidden", children: [copyMode && _jsx(Text, { color: theme.accent, children: "Copy mode \u00B7 drag to select \u00B7 Esc / Ctrl+S resumes" }), _jsx(Frozen, { frozen: displayPaused, identity: `${width}:${state.sessionId}`, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { width: width, children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsx(Text, { bold: true, color: theme.accent, wrap: "truncate-end", children: toolLine(headerTitle, width) }) }), width >= 60 && controller.sessionMode && _jsx(Box, { flexShrink: 0, marginLeft: 2, children: _jsx(Text, { color: theme.accent, children: toolLine(controller.sessionMode, Math.min(24, Math.floor(width / 3))) }) })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(width) })] }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [historyLoading && _jsxs(Text, { dimColor: true, children: [historyLoading, " \u00B7 Esc / Ctrl+C cancel"] }), _jsx(Frozen, { frozen: displayPaused, identity: state.sessionId ?? "", children: statusNotice && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(state.status) }) }), state.error && _jsx(Text, { color: theme.colors.error, children: state.error }), state.screen === 'chat' && _jsxs(Box, { ref: conversationBox, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", marginY: dialogOpen ? 0 : 1, children: [visible.length ? _jsx(Frozen, { frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}:${pageSize}`, children: _jsx(HistoryViewport, { rows: visible }) }) : _jsx(Text, { color: theme.colors.muted, children: "Start a conversation with the host agent." }), showHistoryHint && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: dialogOpen ? 'Wheel/PgUp/PgDn · Scroll history' : historyWindow ? 'Earlier history · /latest returns to live conversation' : 'Scroll up or /older to load earlier history' })] })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [notice && _jsx(Text, { dimColor: true, children: safeText(notice) }), _jsxs(Box, { borderStyle: "round", borderColor: pending ? theme.colors.context : state.online ? theme.accent : theme.border, paddingX: 1, flexDirection: "column", flexShrink: 1, minHeight: 3, children: [_jsx(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: queueOpen && !pending ? _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pending input \u00B7 Esc close" }), !queued.length && _jsx(Text, { dimColor: true, children: state.controlError ? 'Host queue unavailable' : 'No pending input' }), _jsx(Picker, { choices: queued.map(item => ({
|
|
764
|
+
key: item.id, label: toolLine(item.text, width - 6),
|
|
765
|
+
action: () => operate(() => controller.removeQueued(item.id)),
|
|
766
|
+
remove: () => operate(() => controller.removeQueued(item.id)),
|
|
767
|
+
})), pageSize: Math.max(1, Math.min(6, (stdout.rows ?? 30) - 12)), hint: "\u2191 \u2193 select \u00B7 Enter / d / Delete remove \u00B7 Esc close", enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online && !controller.state.pending.length })] }) : removal ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.context, children: removal.kind === 'workspace' ? 'Remove workspace registration?' : 'Archive session?' }), _jsx(Text, { wrap: "truncate-end", children: safeText(removal.name) }), _jsxs(Text, { wrap: "truncate-end", children: ["ID: ", safeText(removal.id)] }), removal.path && _jsxs(Text, { wrap: "truncate-end", children: ["Path: ", safeText(removal.path)] }), _jsx(Text, { children: removal.kind === 'workspace' ? 'Removes the workspace from the list. Directory and sessions are kept.' : 'Hides the session from lists. History is kept; /resume ID can reopen it.' }), _jsx(Text, { dimColor: true, children: "Running tasks continue. Esc cancels this dialog." }), _jsx(Picker, { choices: [
|
|
768
|
+
{ key: 'cancel', label: 'Cancel', action: () => setRemoval(undefined) },
|
|
769
|
+
{ key: 'confirm', label: removal.kind === 'workspace' ? 'Remove workspace' : 'Archive session',
|
|
770
|
+
action: () => operate(async () => { await controller.removeTarget(removal); setRemoval(undefined); }) },
|
|
771
|
+
], enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online }, `${removal.kind}:${removal.id}`)] }) : models ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: models.model ? 'Choose reasoning effort' : 'Choose model' }), _jsx(Text, { dimColor: true, children: "Applies to subsequent requests; host also saves the default." }), array(models.catalog.failures).map(object).map(failure => _jsx(Text, { color: theme.colors.error, children: safeText(`${failure.name}: ${failure.message}`) }, string(failure.id))), _jsx(Picker, { pageSize: Math.max(1, Math.min(8, (stdout.rows ?? 30) - 15)), choices: models.model ? [
|
|
772
|
+
{ key: 'default', label: `Default effort${object(models.model.reasoning).defaultEffort ? ` · ${string(object(models.model.reasoning).defaultEffort)}` : ''}`,
|
|
773
|
+
action: () => operate(async () => { await controller.selectModel(models.provider, string(models.model.id)); setModels(undefined); }) },
|
|
774
|
+
...array(object(models.model.reasoning).efforts).map(object).map(effort => ({ key: string(effort.id), label: toolLine(`${effort.name} (${effort.id})${effort.description ? ` · ${effort.description}` : ''}`, width - 2),
|
|
775
|
+
action: () => operate(async () => { await controller.selectModel(models.provider, string(models.model.id), string(effort.id)); setModels(undefined); }) })),
|
|
776
|
+
{ key: 'back', label: '← Models', action: () => setModels({ catalog: models.catalog }) },
|
|
777
|
+
] : [
|
|
778
|
+
...array(models.catalog.groups).map(object).flatMap(group => array(group.models).map(object).map(model => ({
|
|
779
|
+
key: `${group.id}:${model.id}`, label: toolLine(`${group.name} · ${model.name} (${model.id})`, width - 2),
|
|
780
|
+
action: () => {
|
|
781
|
+
if (array(object(model.reasoning ?? { efforts: [] }).efforts).length)
|
|
782
|
+
setModels({ catalog: models.catalog, provider: string(group.id), model });
|
|
783
|
+
else
|
|
784
|
+
operate(async () => { await controller.selectModel(string(group.id), string(model.id)); setModels(undefined); });
|
|
785
|
+
},
|
|
786
|
+
}))),
|
|
787
|
+
{ key: 'close', label: '← Back to conversation', action: () => setModels(undefined) },
|
|
788
|
+
], enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online }, models.model ? `${models.provider}:${models.model.id}` : 'models')] }) : searchResults ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Session search \u00B7 ", safeText(searchResults.query)] }), searchResults.hasMore && _jsx(Text, { color: theme.colors.context, children: "Host returned only the first 20 global matches; workspace results may be incomplete. Refine your query." }), !searchResults.items.length && _jsx(Text, { children: "No sessions in the returned results" }), _jsx(Picker, { choices: [...searchResults.items.map(item => ({ key: string(item.sessionId),
|
|
789
|
+
label: toolLine(`${item.sessionId} · ${item.snippet}`, width - 2),
|
|
790
|
+
action: () => operate(() => openSearchSession(string(item.sessionId), searchResults.query)),
|
|
791
|
+
})), { key: 'close', label: '← Back', action: () => setSearchResults(undefined) }], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy })] }) : state.screen === 'workspaces' || state.screen === 'sessions' ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: state.screen === 'workspaces' ? 'Choose workspace' : state.showAllSessions ? 'Choose session · All workspaces' : 'Choose session' }), _jsx(Picker, { choices: choices, enabled: state.online && !state.busy && !input, canSelect: () => !draft.current && controller.state.online && !controller.state.busy }, `${state.screen}:${state.workspaceId ?? ''}`)] }) : _jsxs(_Fragment, { children: [thoughtList && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.reasoning, children: "Reasoning history \u00B7 User prompts \u00B7 Esc close" }), !thoughtEntries?.length && !liveThought && _jsx(Text, { dimColor: true, children: "No reasoning in loaded history" }), _jsx(Picker, { choices: thoughtOptions, pageSize: Math.max(1, Math.min(6, Math.floor(((stdout.rows ?? 30) - 14) / 2))), enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `thoughts:${state.sessionId}`)] }), state.screen === 'chat' && historyQuery !== undefined && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: [contentSearch ? 'Search · session history' : 'History · loaded records', " \u00B7 Esc close"] }), contentSearch && historyMatches?.truncated && _jsx(Text, { color: theme.colors.context, children: "Showing the first 200 matches \u00B7 Refine your search" }), _jsx(Picker, { choices: [
|
|
792
|
+
...(contentSearch && historyMatches ? historyMatches.items.map(message => ({
|
|
793
|
+
key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.preview}`, width - 2),
|
|
794
|
+
action: () => operate(() => jumpHistory(message.seq)),
|
|
795
|
+
})) : layout.messages.filter(message => message.role === 'You' && `${message.seq} ${message.text}`.toLowerCase().includes(historyQuery.toLowerCase())).map(message => ({
|
|
796
|
+
key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.text}`, width - 2),
|
|
797
|
+
action: () => operate(() => jumpHistory(message.seq)),
|
|
798
|
+
}))),
|
|
799
|
+
{ key: 'close', label: '← Back to conversation', action: () => { setHistoryQuery(undefined); setHistoryMatches(undefined); } },
|
|
800
|
+
], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `history:${historyQuery}`)] }), pending && _jsxs(Box, { flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, color: theme.colors.context, children: question ? `Question ${answered.length + 1}/${questions.length}${question.header ? ` · ${safeText(string(question.header))}` : ''}` : 'Approval required' }), _jsx(Text, { children: safeText(question ? string(question.question) : JSON.stringify(pending.request, null, 2)) }), question?.detail && _jsx(Text, { children: safeText(string(question.detail)) }), options.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [[...options, { label: 'Other answer — type below' }].map((option, index) => ({ option, index }))
|
|
801
|
+
.slice(optionStart, optionStart + optionPageSize).map(({ option, index }) => _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { color: index === optionCursor ? theme.accent : undefined, wrap: "truncate-end", children: [index === optionCursor ? '❯ ' : ' ', index + 1, ". ", question?.multiSelect === true && index < options.length ? choiceState.selected.includes(string(option.label)) ? '[x] ' : '[ ] ' : '', safeText(string(option.label))] }), option.description && _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [' ', safeText(string(option.description))] })] }, index)), _jsx(Text, { dimColor: true, children: choiceState.custom ? 'Type your answer below · Esc returns to options' : question?.multiSelect === true
|
|
802
|
+
? '↑ ↓ move · Space / 1–9 toggle · Enter confirm' : '↑ ↓ / 1–9 select · Enter confirm' })] }), _jsx(Text, { dimColor: true, children: question ? 'Text answers supported · Ctrl+C clears · /cancel stops' : '/allow approves once · /deny rejects · /cancel stops' })] })] }) }), !queueOpen && !pending && state.screen === 'chat' && queued.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { dimColor: true, children: ["Waiting: ", queued.length, " \u00B7 /queue to remove"] }), queued.slice(0, 2).map(item => _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [item.placement === 'steering' ? '↳ ' : '· ', safeText(toolLine(item.text, width - 6))] }, item.id))] }), _jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: theme.accent, children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: setCursor, onSubmit: () => { void submit(draft.current); }, reservedKeys: queueOpen && !pending ? ['d'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : undefined, focus: state.online && !state.busy && !copyMode, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' })] }), referenceOpen && _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Host files \u00B7 \u2191 \u2193 select \u00B7 Tab/Enter insert \u00B7 Esc close" }), !matches ? _jsx(Text, { dimColor: true, children: "Searching\u2026" }) : matches.error ? _jsx(Text, { color: theme.colors.error, children: matches.error })
|
|
803
|
+
: matches.items.length === 0 ? _jsx(Text, { dimColor: true, children: "No matching host files" })
|
|
804
|
+
: matches.items.slice(Math.max(0, referenceIndex - 5), Math.max(0, referenceIndex - 5) + 6).map((item, index) => _jsxs(Text, { color: index + Math.max(0, referenceIndex - 5) === referenceIndex ? theme.accent : undefined, children: [index + Math.max(0, referenceIndex - 5) === referenceIndex ? '❯ ' : ' ', item.path, item.kind === 'directory' ? '/' : ''] }, `${item.kind}:${item.path}`))] })] }), input.startsWith('/') && !input.includes(' ') && _jsx(Text, { dimColor: true, children: COMMANDS.filter(command => command.startsWith(input)).join(' ') }), help && _jsxs(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [COMMAND_HINTS.slice(currentHelpPage * helpPageSize, (currentHelpPage + 1) * helpPageSize).map((hint, index) => _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: theme.accent, children: COMMAND_LABELS[currentHelpPage * helpPageSize + index].padEnd(COMMAND_LABEL_WIDTH) }), hint.description] }, hint.command)), _jsxs(Text, { dimColor: true, children: ["Help ", currentHelpPage + 1, "/", helpPages, " \u00B7 PgUp/PgDn pages \u00B7 Esc close"] }), _jsx(Text, { dimColor: true, children: "Enter send \u00B7 Tab complete \u00B7 Esc cancel \u00B7 Wheel/PgUp/PgDn scroll \u00B7 Ctrl+C clear / stop / exit" }), _jsx(Text, { dimColor: true, children: "History: \u2191/\u2193 or Ctrl+P/N recall \u00B7 Editing: Ctrl+A/E start/end \u00B7 Ctrl+K/U kill right/left \u00B7 Ctrl+W kill word \u00B7 Ctrl+Y restore" })] }), costExpanded && _jsx(CostPanel, { controller: controller }), _jsx(Frozen, { frozen: displayPaused || position > 0, identity: `${width}:${state.sessionId}:${statusExpanded}`, children: _jsx(StatusBar, { controller: controller, width: width, expanded: statusExpanded, revision: state.version, paused: displayPaused || position > 0 }) })] })] }) }) }) });
|
|
746
805
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -37,13 +37,20 @@ export declare class Client {
|
|
|
37
37
|
cookie: string;
|
|
38
38
|
expiresAt: number;
|
|
39
39
|
} | undefined;
|
|
40
|
+
/** Download the authenticated host ZIP without using a mutation or ordinary RPC deadline.
|
|
41
|
+
* @param sessionId - Selected session identity.
|
|
42
|
+
* @param signal - Cancels the streaming response.
|
|
43
|
+
* @returns Response whose body the caller must consume or cancel.
|
|
44
|
+
*/
|
|
45
|
+
sessionLog(sessionId: string, signal: AbortSignal): Promise<Response>;
|
|
40
46
|
/** Invoke an exact endpoint once. Mutations are never automatically retried.
|
|
41
47
|
* @param endpoint - Namespace/method endpoint.
|
|
42
48
|
* @param args - Host parameter names and JSON values.
|
|
43
49
|
* @param signal - Optional caller cancellation, combined with client close and timeout.
|
|
50
|
+
* @param timeoutMs - Per-call timeout; null waits for caller cancellation or client close.
|
|
44
51
|
* @returns The decoded result value; HTTP, remote, and cancellation errors reject.
|
|
45
52
|
*/
|
|
46
|
-
call(endpoint: string, args?: ObjectValue, signal?: AbortSignal): Promise<Json | undefined>;
|
|
53
|
+
call(endpoint: string, args?: ObjectValue, signal?: AbortSignal, timeoutMs?: number | null): Promise<Json | undefined>;
|
|
47
54
|
/** Connect the physical mux. A disconnected instance may reconnect with its cookie. */
|
|
48
55
|
connect(): Promise<void>;
|
|
49
56
|
/** Subscribe on the existing mux; each subscription has a fresh stream identity. */
|
package/dist/client.js
CHANGED
|
@@ -69,18 +69,35 @@ export class Client {
|
|
|
69
69
|
get persistentCookie() {
|
|
70
70
|
return this.expiresAt === undefined ? undefined : { cookie: this.cookie, expiresAt: this.expiresAt };
|
|
71
71
|
}
|
|
72
|
+
/** Download the authenticated host ZIP without using a mutation or ordinary RPC deadline.
|
|
73
|
+
* @param sessionId - Selected session identity.
|
|
74
|
+
* @param signal - Cancels the streaming response.
|
|
75
|
+
* @returns Response whose body the caller must consume or cancel.
|
|
76
|
+
*/
|
|
77
|
+
async sessionLog(sessionId, signal) {
|
|
78
|
+
const url = new URL('/api/session.export', this.base);
|
|
79
|
+
url.searchParams.set('sessionId', sessionId);
|
|
80
|
+
const response = await fetch(url, { redirect: 'error', headers: { cookie: this.cookie },
|
|
81
|
+
signal: AbortSignal.any([this.lifetime.signal, signal]) });
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
await response.body?.cancel();
|
|
84
|
+
throw new HttpError(response.status, 'Session log export');
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
72
88
|
/** Invoke an exact endpoint once. Mutations are never automatically retried.
|
|
73
89
|
* @param endpoint - Namespace/method endpoint.
|
|
74
90
|
* @param args - Host parameter names and JSON values.
|
|
75
91
|
* @param signal - Optional caller cancellation, combined with client close and timeout.
|
|
92
|
+
* @param timeoutMs - Per-call timeout; null waits for caller cancellation or client close.
|
|
76
93
|
* @returns The decoded result value; HTTP, remote, and cancellation errors reject.
|
|
77
94
|
*/
|
|
78
|
-
async call(endpoint, args = {}, signal) {
|
|
95
|
+
async call(endpoint, args = {}, signal, timeoutMs = this.timeoutMs) {
|
|
79
96
|
if (!/^[\w$-]+\/[\w$-]+$/.test(endpoint))
|
|
80
97
|
throw new Error('Invalid RPC endpoint');
|
|
81
98
|
const rpcId = randomUUID();
|
|
82
99
|
const response = await fetch(new URL(`/api/${endpoint}`, this.base), {
|
|
83
|
-
method: 'POST', redirect: 'error', signal: signal ? AbortSignal.any([this.signal(), signal]) : this.signal(),
|
|
100
|
+
method: 'POST', redirect: 'error', signal: signal ? AbortSignal.any([this.signal(timeoutMs), signal]) : this.signal(timeoutMs),
|
|
84
101
|
headers: { 'content-type': 'application/json', cookie: this.cookie },
|
|
85
102
|
body: JSON.stringify({ type: 'client-request', rpcId, method: endpoint, payload: { args } }),
|
|
86
103
|
});
|
|
@@ -215,8 +232,8 @@ export class Client {
|
|
|
215
232
|
await closed;
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
|
-
signal() {
|
|
219
|
-
return AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(
|
|
235
|
+
signal(timeoutMs = this.timeoutMs) {
|
|
236
|
+
return timeoutMs === null ? this.lifetime.signal : AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(timeoutMs)]);
|
|
220
237
|
}
|
|
221
238
|
fail(error) {
|
|
222
239
|
const listeners = [...this.listeners.values()];
|
package/dist/controller.d.ts
CHANGED
|
@@ -183,8 +183,24 @@ export declare class Controller {
|
|
|
183
183
|
* @returns Validated candidates in host order.
|
|
184
184
|
*/
|
|
185
185
|
references(query: string, signal: AbortSignal): Promise<FileReference[]>;
|
|
186
|
-
/**
|
|
187
|
-
|
|
186
|
+
/** Execute a human command directly, outside the model prompt queue.
|
|
187
|
+
* @param line - Complete slash command, including arguments.
|
|
188
|
+
* @param signal - Cancels the request while the host performs compaction.
|
|
189
|
+
* @returns The host's successful command result text.
|
|
190
|
+
*/
|
|
191
|
+
command(line: string, signal: AbortSignal): Promise<string>;
|
|
192
|
+
/** Remove one host-owned pending input; an already claimed item reports a host error.
|
|
193
|
+
* @param itemId - Queue occurrence identity from session/control.
|
|
194
|
+
*/
|
|
195
|
+
removeQueued(itemId: string): Promise<void>;
|
|
196
|
+
/** Export the selected host log to a new local ZIP file.
|
|
197
|
+
* @param path - Optional local destination; existing files are never overwritten.
|
|
198
|
+
* @param signal - Cancels the download and removes an incomplete file.
|
|
199
|
+
* @returns Absolute saved filename.
|
|
200
|
+
*/
|
|
201
|
+
exportLog(path: string | undefined, signal: AbortSignal): Promise<string>;
|
|
202
|
+
/** Admit text once as steering while running, or a new turn while idle; a lost response can leave delivery uncertain. */
|
|
203
|
+
prompt(text: string): Promise<void>;
|
|
188
204
|
/** Cancel the active turn; pending queue items remain host-owned. */
|
|
189
205
|
cancelTurn(): Promise<void>;
|
|
190
206
|
/** Add a page before the retained window using its fixed opening cut. */
|
package/dist/controller.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** UI state and connection generations for the standalone terminal client. */
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { saveSessionLog } from "./export.js";
|
|
3
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
5
|
import { Client, HttpError, RemoteError } from "./client.js";
|
|
5
6
|
import { AuthenticationRequired } from "./auth.js";
|
|
@@ -564,13 +565,50 @@ export class Controller {
|
|
|
564
565
|
async references(query, signal) {
|
|
565
566
|
return fileReferences(await this.host.call('fileReferences/list', { agentId: this.sessionId, query }, signal));
|
|
566
567
|
}
|
|
567
|
-
/**
|
|
568
|
-
|
|
568
|
+
/** Execute a human command directly, outside the model prompt queue.
|
|
569
|
+
* @param line - Complete slash command, including arguments.
|
|
570
|
+
* @param signal - Cancels the request while the host performs compaction.
|
|
571
|
+
* @returns The host's successful command result text.
|
|
572
|
+
*/
|
|
573
|
+
async command(line, signal) {
|
|
574
|
+
if (!this.state.transcript.ready)
|
|
575
|
+
throw new Error('Wait for the session snapshot before running commands');
|
|
576
|
+
const execution = await this.host.call('commands/execute', {
|
|
577
|
+
agentId: this.sessionId, line, submittedAttachments: [],
|
|
578
|
+
}, signal, null);
|
|
579
|
+
if (execution === undefined)
|
|
580
|
+
throw new Error(`This host does not provide ${line.split(/\s/, 1)[0]}`);
|
|
581
|
+
const result = object(object(execution).result);
|
|
582
|
+
if ((result.kind !== 'success' && result.kind !== 'error') || (result.text !== undefined && typeof result.text !== 'string')) {
|
|
583
|
+
throw new Error('Invalid command result from host');
|
|
584
|
+
}
|
|
585
|
+
if (result.kind === 'error')
|
|
586
|
+
throw new Error(string(result.text));
|
|
587
|
+
return result.text === undefined ? 'Command completed.' : string(result.text);
|
|
588
|
+
}
|
|
589
|
+
/** Remove one host-owned pending input; an already claimed item reports a host error.
|
|
590
|
+
* @param itemId - Queue occurrence identity from session/control.
|
|
591
|
+
*/
|
|
592
|
+
async removeQueued(itemId) {
|
|
593
|
+
await this.host.call('session/updateQueue', { request: { sessionId: this.sessionId, itemId, action: { kind: 'remove' } } });
|
|
594
|
+
}
|
|
595
|
+
/** Export the selected host log to a new local ZIP file.
|
|
596
|
+
* @param path - Optional local destination; existing files are never overwritten.
|
|
597
|
+
* @param signal - Cancels the download and removes an incomplete file.
|
|
598
|
+
* @returns Absolute saved filename.
|
|
599
|
+
*/
|
|
600
|
+
async exportLog(path, signal) {
|
|
601
|
+
return saveSessionLog(this.host, this.sessionId, path, signal);
|
|
602
|
+
}
|
|
603
|
+
/** Admit text once as steering while running, or a new turn while idle; a lost response can leave delivery uncertain. */
|
|
604
|
+
async prompt(text) {
|
|
605
|
+
if (this.state.pending.length)
|
|
606
|
+
throw new Error('Answer the pending question or approval first');
|
|
569
607
|
this.stoppingSession = undefined;
|
|
570
608
|
if (!this.state.transcript.ready)
|
|
571
609
|
throw new Error('Wait for the session snapshot before sending');
|
|
572
610
|
const admission = this.host.call('session/prompt', { request: {
|
|
573
|
-
sessionId: this.sessionId, requestId: randomUUID(), mode,
|
|
611
|
+
sessionId: this.sessionId, requestId: randomUUID(), mode: this.running ? 'steer' : 'queue',
|
|
574
612
|
content: [{ type: 'text', text }], clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
575
613
|
} });
|
|
576
614
|
this.admission = admission;
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from './client.ts';
|
|
2
|
+
/** Save an archive without overwriting an existing file or retaining a partial download.
|
|
3
|
+
* @param client - Authenticated host transport.
|
|
4
|
+
* @param sessionId - Selected session to export.
|
|
5
|
+
* @param destination - Local filename, defaulting to a timestamped ZIP in the working directory.
|
|
6
|
+
* @param signal - Caller-owned cancellation.
|
|
7
|
+
* @returns Absolute saved filename.
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveSessionLog(client: Client, sessionId: string, destination: string | undefined, signal: AbortSignal): Promise<string>;
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Stream authenticated session archives to exclusive local files. */
|
|
2
|
+
import { open, unlink } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
/** Save an archive without overwriting an existing file or retaining a partial download.
|
|
5
|
+
* @param client - Authenticated host transport.
|
|
6
|
+
* @param sessionId - Selected session to export.
|
|
7
|
+
* @param destination - Local filename, defaulting to a timestamped ZIP in the working directory.
|
|
8
|
+
* @param signal - Caller-owned cancellation.
|
|
9
|
+
* @returns Absolute saved filename.
|
|
10
|
+
*/
|
|
11
|
+
export async function saveSessionLog(client, sessionId, destination, signal) {
|
|
12
|
+
const path = resolve(destination ?? `session-${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}-${Date.now()}.zip`);
|
|
13
|
+
const file = await open(path, 'wx', 0o600);
|
|
14
|
+
let complete = false;
|
|
15
|
+
try {
|
|
16
|
+
const response = await client.sessionLog(sessionId, signal);
|
|
17
|
+
if (!response.body)
|
|
18
|
+
throw new Error('Session log export has no body');
|
|
19
|
+
const reader = response.body.getReader();
|
|
20
|
+
async function* chunks() {
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
const chunk = await reader.read();
|
|
24
|
+
if (chunk.done)
|
|
25
|
+
return;
|
|
26
|
+
yield chunk.value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
try {
|
|
31
|
+
await reader.cancel();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
reader.releaseLock();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await file.writeFile(chunks(), { signal });
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
complete = true;
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
try {
|
|
45
|
+
await file.close();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (!complete)
|
|
49
|
+
await unlink(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/history.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare function releaseHistoryLayout(transcript: Transcript): void;
|
|
|
19
19
|
* @param width - Available terminal columns.
|
|
20
20
|
* @param reasoning - Global committed/completed reasoning fold mode.
|
|
21
21
|
* @param overrides - Sequences whose fold mode differs from the global mode; replace the set on changes.
|
|
22
|
-
* @param liveReasoning - Fold mode for completed blocks
|
|
22
|
+
* @param liveReasoning - Fold mode for completed live blocks; below 60 content columns it also folds active reasoning.
|
|
23
23
|
* @returns Row count, sequence offsets, and a viewport reader; `lines` materializes all rows for exports only.
|
|
24
24
|
*/
|
|
25
25
|
export declare function historyLayout(transcript: Transcript, width: number, reasoning?: Reasoning, overrides?: ReadonlySet<number>, liveReasoning?: Reasoning): {
|
package/dist/history.js
CHANGED
|
@@ -116,7 +116,7 @@ function partRows(parts, width, reasoning, seq) {
|
|
|
116
116
|
const cached = seq === undefined ? liveRows.get(part) : undefined;
|
|
117
117
|
if (cached?.width === width && cached.reasoning === reasoning)
|
|
118
118
|
return cached.rows;
|
|
119
|
-
const fold = part.kind === 'reasoning' && reasoning === 'row' && (seq !== undefined || part.closed);
|
|
119
|
+
const fold = part.kind === 'reasoning' && reasoning === 'row' && (seq !== undefined || part.closed || width < 60);
|
|
120
120
|
const text = fold ? toolLine(`◇ /think${seq === undefined ? ' live' : ` ${seq}`} · ${part.text.slice(2)}`, width) : part.text;
|
|
121
121
|
const rows = wrapAnsi(text, width, { hard: true, trim: !['tool', 'success', 'error'].includes(part.kind) }).split('\n').map(text => ({ text, kind: part.kind, seq }));
|
|
122
122
|
if (seq === undefined)
|
|
@@ -147,7 +147,7 @@ export function releaseHistoryLayout(transcript) {
|
|
|
147
147
|
* @param width - Available terminal columns.
|
|
148
148
|
* @param reasoning - Global committed/completed reasoning fold mode.
|
|
149
149
|
* @param overrides - Sequences whose fold mode differs from the global mode; replace the set on changes.
|
|
150
|
-
* @param liveReasoning - Fold mode for completed blocks
|
|
150
|
+
* @param liveReasoning - Fold mode for completed live blocks; below 60 content columns it also folds active reasoning.
|
|
151
151
|
* @returns Row count, sequence offsets, and a viewport reader; `lines` materializes all rows for exports only.
|
|
152
152
|
*/
|
|
153
153
|
export function historyLayout(transcript, width, reasoning = 'row', overrides = noOverrides, liveReasoning = reasoning) {
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
|
|
2
2
|
import { type ObjectValue } from './wire.ts';
|
|
3
|
+
/** One host-owned pending input, removable only while its occurrence remains queued. */
|
|
4
|
+
export interface QueuedInput {
|
|
5
|
+
id: string;
|
|
6
|
+
placement: 'queued' | 'steering' | 'context';
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
3
9
|
/** Generation-local projection, inbox and background-job data for every session. */
|
|
4
10
|
export declare class Telemetry {
|
|
5
11
|
private readonly retainedKeys?;
|
|
@@ -27,5 +33,10 @@ export declare class Telemetry {
|
|
|
27
33
|
queued?: number;
|
|
28
34
|
jobs?: number;
|
|
29
35
|
};
|
|
36
|
+
/** Read the selected session's authoritative pending inputs; absence means no observed queue.
|
|
37
|
+
* @param id - Session identity.
|
|
38
|
+
* @returns Pending inputs in host order.
|
|
39
|
+
*/
|
|
40
|
+
pending(id?: string): readonly QueuedInput[];
|
|
30
41
|
private entry;
|
|
31
42
|
}
|
package/dist/telemetry.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/** Host projection values with per-key watermarks; snapshots never roll back newer updates. */
|
|
2
2
|
import { array, object, string } from "./wire.js";
|
|
3
|
+
function queuedInputs(value) {
|
|
4
|
+
return array(value).map(value => {
|
|
5
|
+
const item = object(value);
|
|
6
|
+
if (!['queued', 'steering', 'context'].includes(string(item.placement)))
|
|
7
|
+
throw new Error('Invalid queue placement');
|
|
8
|
+
return { id: string(item.id), placement: item.placement,
|
|
9
|
+
text: array(object(item.message).content).map(object).map(block => block.type === 'text' ? string(block.text) : `[${string(block.type)}]`).join(' ') };
|
|
10
|
+
});
|
|
11
|
+
}
|
|
3
12
|
/** Generation-local projection, inbox and background-job data for every session. */
|
|
4
13
|
export class Telemetry {
|
|
5
14
|
retainedKeys;
|
|
@@ -24,7 +33,7 @@ export class Telemetry {
|
|
|
24
33
|
for (const [id, projection] of Object.entries(object(baseline.projections)))
|
|
25
34
|
this.snapshot(id, projection);
|
|
26
35
|
for (const [id, items] of Object.entries(object(baseline.queues)))
|
|
27
|
-
this.queues.set(id,
|
|
36
|
+
this.queues.set(id, queuedInputs(items));
|
|
28
37
|
for (const [id, items] of Object.entries(object(baseline.jobs)))
|
|
29
38
|
this.jobs.set(id, activeJobs(items));
|
|
30
39
|
this.ready = true;
|
|
@@ -47,7 +56,7 @@ export class Telemetry {
|
|
|
47
56
|
entry.revisions.set(key, seq);
|
|
48
57
|
}
|
|
49
58
|
else if (frame.type === 'queue')
|
|
50
|
-
this.queues.set(id,
|
|
59
|
+
this.queues.set(id, queuedInputs(frame.items));
|
|
51
60
|
else if (frame.type === 'jobs')
|
|
52
61
|
this.jobs.set(id, activeJobs(frame.items));
|
|
53
62
|
else
|
|
@@ -85,8 +94,13 @@ export class Telemetry {
|
|
|
85
94
|
*/
|
|
86
95
|
view(id) {
|
|
87
96
|
return { values: id ? this.entries.get(id)?.values ?? {} : {},
|
|
88
|
-
queued: id ? this.queues.get(id) : undefined, jobs: id ? this.jobs.get(id) : undefined };
|
|
97
|
+
queued: id ? this.queues.get(id)?.length : undefined, jobs: id ? this.jobs.get(id) : undefined };
|
|
89
98
|
}
|
|
99
|
+
/** Read the selected session's authoritative pending inputs; absence means no observed queue.
|
|
100
|
+
* @param id - Session identity.
|
|
101
|
+
* @returns Pending inputs in host order.
|
|
102
|
+
*/
|
|
103
|
+
pending(id) { return id ? this.queues.get(id) ?? [] : []; }
|
|
90
104
|
entry(id) {
|
|
91
105
|
let entry = this.entries.get(id);
|
|
92
106
|
if (!entry) {
|
package/dsht-m.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itookit/dsht",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remote-first TUI for DeepSeek Harness with SSH-friendly mobile access and request-level cost tracking",
|
|
6
6
|
"engines": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"dist",
|
|
14
14
|
"README.md",
|
|
15
15
|
"README.zh.md",
|
|
16
|
-
"dsht.png"
|
|
16
|
+
"dsht-m.png"
|
|
17
17
|
],
|
|
18
18
|
"main": "dist/client.js",
|
|
19
19
|
"types": "dist/client.d.ts",
|
package/dsht.png
DELETED
|
Binary file
|