@itookit/dsht 0.3.3 → 0.3.4
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 +9 -9
- package/README.zh.md +6 -6
- package/dist/cli/dsht.d.ts +2 -0
- package/dist/cli/dsht.js +125 -0
- package/dist/cli/index.js +16 -123
- package/dist/controller/controller.d.ts +8 -0
- package/dist/controller/controller.js +12 -0
- package/dist/controller/perf-measures.d.ts +34 -0
- package/dist/controller/perf-measures.js +78 -0
- package/dist/session/controller.d.ts +16 -0
- package/dist/session/controller.js +33 -0
- package/dist/session/navigation.d.ts +62 -8
- package/dist/session/navigation.js +72 -13
- package/dist/ui/app.js +53 -11
- package/dist/ui/chat/status.js +12 -7
- package/dist/ui/dialogs/index.d.ts +6 -2
- package/dist/ui/dialogs/index.js +3 -3
- package/dist/ui/dialogs/picker.d.ts +21 -3
- package/dist/ui/dialogs/picker.js +37 -5
- package/dist/ui/input/input.d.ts +18 -3
- package/dist/ui/input/input.js +61 -22
- package/dist/ui/input/viewport.d.ts +96 -0
- package/dist/ui/input/viewport.js +173 -0
- package/package.json +3 -3
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: fea06c629ad8eb11b815d375d3bbcf4fc5451dd3
|
|
3
|
+
README.zh.md: f9209f005a9c78bd2db19e0c1bbb59a6603ad7e9
|
package/README.md
CHANGED
|
@@ -279,8 +279,8 @@ From a source checkout, run the same commands through npm or through the source
|
|
|
279
279
|
```sh
|
|
280
280
|
npm start -- list workspaces --json
|
|
281
281
|
npm start -- list sessions --json
|
|
282
|
-
node --import tsx src/cli/index.
|
|
283
|
-
node --import tsx src/cli/index.
|
|
282
|
+
node --import tsx src/cli/index.ts list workspaces --json
|
|
283
|
+
node --import tsx src/cli/index.ts list sessions --json
|
|
284
284
|
```
|
|
285
285
|
|
|
286
286
|
JSON output is `{ "items": [...] }`; omit `--json` for tab-separated output. Workspace filtering uses the host's `sessionIds` membership. The workspace list consumes and cancels the first `workspace/follow` baseline; it does not call a nonexistent `workspace/list` endpoint.
|
|
@@ -293,7 +293,7 @@ Enter submits a prompt: while the agent is Working it becomes steering for the n
|
|
|
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
|
|
|
296
|
-
In `/ws` and `/resume` pickers,
|
|
296
|
+
In `/ws` and `/resume` pickers, every row reports one of three user-visible states, most actionable first: `?` needs you (this client holds an unanswered approval or question for that session), `◐` working, and `●` ready, each followed by the age of its last activity. A workspace row rolls its sessions up in that order, leaves out sessions that never sent a turn, and on a wide terminal spells the states out beside a right-aligned directory column; a narrow terminal keeps `?1 ◐2 ●6` and prints the marker key under the title. Select a workspace or session and press `d` or Delete with an empty composer to review removal. With a draft, `d` remains normal input; Backspace never opens removal. `/ws --delete <name or ID>` and `/resume --delete <title or ID>` open the same confirmation; `/resume --archive <title or ID>` is an alias for session archival. Before session removal the client refreshes `session/list`. A session explicitly marked `blank: true` and idle, with no known queued jobs or pending local prompt admission, is archived immediately without confirmation. This uses the host blank flag rather than the loaded history window or title. Other sessions still require confirmation: Cancel is selected by default, and Escape closes the dialog. Workspace removal calls `workspace/delete` and removes only its registration: directories and sessions remain. Session removal calls `workspace/archiveSession`, hiding the session from workspace lists and `/resume all` while retaining history; `/resume ID` can reopen it. This host API exposes archival rather than permanent session deletion. Running tasks continue. Archiving the selected session releases its transcript and layout caches; rejected operations retain the list and confirmation for retry.
|
|
297
297
|
|
|
298
298
|
`/search` matches literal text case-insensitively in conversation messages, including older pages; tool-only rows are excluded. Search scans up to 80 messages per request, discards each temporary page, and retains at most 200 short matches, including folded reasoning. A truncated result asks you to refine the query. Opening a match loads a separate page around its sequence; `/latest` releases that window. Esc or Ctrl+C cancels the search. A rare or absent term still requires scanning the full history over HTTP; there is no server-side full-text index for this command. `/history` lists your own prompts from the loaded pages. Record sequences are the numbers shown by these pickers. `/ssearch` and `/wsearch` call `session/search`, which searches current user/assistant message content and returns at most 20 sessions, snippets, and a truncation flag; it exposes neither a result cursor nor matching record sequences. Workspace filtering happens after that global limit, so a truncated workspace result can omit matches. The UI warns when results are incomplete; refine the query. Selecting a session loads its history and offers matching messages for the jump. These operations use HTTP and never scan the host configuration directory.
|
|
299
299
|
|
|
@@ -303,7 +303,7 @@ Within each User group, only the first assistant prose or reasoning message show
|
|
|
303
303
|
|
|
304
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
|
-
Tab completes the leading slash command, extending an ambiguous draft to the shared prefix. The
|
|
306
|
+
Tab completes the leading slash command, extending an ambiguous draft to the shared prefix. The composer supports Readline-style editing and keeps the line breaks and tabs of pasted text; a tab is displayed at its tab stop and sent unchanged. Words are whitespace-delimited; cursor movement and character deletion preserve composed Unicode characters. The composer shows only a few content rows and scrolls to keep the cursor visible, so a long draft never squeezes the conversation away; its window is sized from the terminal height alone, so a wider terminal only wraps less. A multiline draft taller than that window folds its interior lines behind `[N lines · X KB]` while the first and last lines stay visible; ←/→ cross the block in one step, Backspace or Delete at its edge removes the whole block, and Enter sends the full text. 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
|
|
|
308
308
|
| Key | Edit |
|
|
309
309
|
| --- | --- |
|
|
@@ -366,7 +366,7 @@ Below 62 terminal columns, active streaming reasoning defaults to one folded row
|
|
|
366
366
|
|
|
367
367
|
Approvals offer 1. Allow once, 2. Deny, and 3. Stop turn. With an empty composer, use 1–3 or ↑/↓ to select, then Enter to confirm; no action is selected initially, Escape clears the highlight, and a replayed request starts unselected again. Existing drafts keep normal typing, and `/allow`, `/deny`, and `/cancel` remain available.
|
|
368
368
|
|
|
369
|
-
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
|
|
369
|
+
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 leaves the question: with the options showing it dismisses the whole set, which the host records as a cancellation, while inside Other the first Escape only returns to the options. 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.
|
|
370
370
|
|
|
371
371
|
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.
|
|
372
372
|
|
|
@@ -380,11 +380,11 @@ Closed `mermaid` fences render as Unicode diagrams for supported flowcharts, sta
|
|
|
380
380
|
|
|
381
381
|
## Live status
|
|
382
382
|
|
|
383
|
-
The footer groups `◐ Working · 8s · Ctrl+C Stop
|
|
383
|
+
The footer groups `◐ Working · 8s · Ctrl+C Stop`, `● Ready`, or `? Needs you` while this client still owes an approval or an answer, model and reasoning effort, the session cost beside today's spend with the all-time total, a ten-cell context bar and percentage, and session turns / total tokens / cache-hit share. Wide terminals reserve the activity column so model and metrics stay aligned when a run completes. Narrow terminals reclaim padding, remove the bar, shorten the model, and then omit lower-priority metrics while retaining the stop hint. `/status` shows the host URL, operation status, workspace path, full provider/model, pending model, usage buckets, turns, queues, jobs, and four-decimal costs. `!` flags a metrics or model catalog error, or incomplete cost coverage; details explain the cause. Running sessions use the last-used model; ready sessions use the next selection, with the host catalog default for new sessions. Model catalog changes refresh on host settings, credential, and adapter notifications.
|
|
384
384
|
|
|
385
385
|
Working time uses the retained `turn/start` timestamp. If that timestamp is unavailable, `(observed)` in the details panel means time since this client observed the run; reconnecting can reset this fallback. The clock stops when the host reports idle. The status includes model generation, tool execution, and approval waits, not just streamed text. Offline status is explicitly marked as last known. The expanded panel merges related values onto one row and prints counts in the compact form the single-row bar already uses (`Context ~40% (400.6K/1M) · 229.7M tok`), so a 24-row terminal shows every detail on one screen; ↑/↓ scroll it one line at a time, PgUp/PgDn one screen, and the footer names the visible range and the total.
|
|
386
386
|
|
|
387
|
-
The single-row bar keeps its groups by value rather than by column: `◐ 6:18 · bash 1:08 · ^C │ v4-flash · high · ctx 30% · S¥2.49* · ¥: 5.00 (12.34) · 2 turns · 34.5M tok · hit 92%`. The live phase is reported as a fact — `think 28s` while reasoning, the tool name with its age while a tool runs, and `write 12s` while the answer streams — and never inferred from silence, because a long reasoning step and a quiet tool are not stalls. It names the newest event and keeps its age until another stripe of work starts or the turn closes, so the quiet stretch after a command is still time the turn spent working, and `● Ready` drops the phase because only the host knows the turn ended. The two cost groups never substitute for each other: `S¥2.49*` is this session, and reports `S?` while the ledger has not priced it, while `¥: 5.00 (12.34)` is today's spend with the all-time total. When the width runs out the least valuable group is dropped first (the cache-hit share, tokens, turns, effort, model, today's spend, then context); the session cost is never dropped but moves to a second row, and the state cluster gives up the phase and the stop hint only below about twenty columns. A paused clock names its reason (`⏸ copy`, `⏸ dialog`, `⏸ history`) instead of freezing silently, and `! Offline` or `⚠ Error` replaces the state token entirely.
|
|
387
|
+
The single-row bar keeps its groups by value rather than by column: `◐ 6:18 · bash 1:08 · ^C │ v4-flash · high · ctx 30% · S¥2.49* · ¥: 5.00 (12.34) · 2 turns · 34.5M tok · hit 92%`. The live phase is reported as a fact — `think 28s` while reasoning, the tool name with its age while a tool runs, and `write 12s` while the answer streams — and never inferred from silence, because a long reasoning step and a quiet tool are not stalls. It names the newest event and keeps its age until another stripe of work starts or the turn closes, so the quiet stretch after a command is still time the turn spent working, and `● Ready` drops the phase because only the host knows the turn ended. The two cost groups never substitute for each other: `S¥2.49*` is this session, and reports `S?` while the ledger has not priced it, while `¥: 5.00 (12.34)` is today's spend with the all-time total. When the width runs out the least valuable group is dropped first (the cache-hit share, tokens, turns, effort, model, today's spend, then context); the session cost is never dropped but moves to a second row, and the state cluster gives up the phase and the stop hint only below about twenty columns. A paused clock names its reason (`⏸ copy`, `⏸ dialog`, `⏸ history`) instead of freezing silently, and `! Offline` or `⚠ Error` replaces the state token entirely. While this client still owes an approval or an answer, `? Needs you` takes the token over instead — including while a dialog has paused the clock, because the owed answer is the thing to act on.
|
|
388
388
|
|
|
389
389
|
Turns come from the complete session's `sessionStats.turns` projection. Context occupancy is marked `~`: Harness combines provider usage with estimated surface changes and the latest route capacity. Token totals come from the complete session's `tokenUsage` projection, with separate uncached input, output, cache-read, and cache-write buckets; reasoning is already included in output. The cache-hit share is the cache-read bucket over all three billed prompt buckets, and it gains decimal places rather than reporting a partial hit as `100%`. Session and day costs come from the ledger's per-session slices, so a session with no slice yet is unknown, not the day's spend. Totals update when the host publishes usage, not on every streamed character. Missing measurements display `unknown` or `?`. Control-stream baselines replace state on reconnect, and per-key watermarks prevent an older follow snapshot from overwriting newer metrics.
|
|
390
390
|
|
|
@@ -394,7 +394,7 @@ Reasoning streams in full while being generated, then folds when its block close
|
|
|
394
394
|
|
|
395
395
|
`/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.
|
|
396
396
|
|
|
397
|
-
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. A bounded runtime memory log is enabled by default at `<state>/memory.log`: one JSON line every 30 seconds with the process counters, the retained record and byte counts, the pin state, the ledger size, the layout row cache, the bounded math and diagram cache, and the sessions, pages and events the last cost scan re-read, so growth can be told apart from V8's high-water mark. On a runtime that exposes a collection the sample also records the heap after a forced one; `npm run start:profile` supplies that runtime together with a heap-snapshot signal, where `kill -USR2 <pid>` writes a snapshot. `/coredump [tag]` writes the same `.heapsnapshot` artifact into the client's working directory without a signal or a profiling runtime; the tag labels the file (default `snapshot`) and V8 serializes the heap synchronously, so the client pauses until the file exists. Open the snapshot in Chrome DevTools rather than the terminal. `--memory-log <path>` or `DSHT_MEMORY_LOG` changes the path, and `--no-memory-log` or `DSHT_MEMORY_LOG=off` disables it. 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.
|
|
397
|
+
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. A bounded runtime memory log is enabled by default at `<state>/memory.log`: one JSON line every 30 seconds with the process counters, the retained record and byte counts, the pin state, the ledger size, the layout row cache, the bounded math and diagram cache, the React render-measurement count, and the sessions, pages and events the last cost scan re-read, so growth can be told apart from V8's high-water mark. On a runtime that exposes a collection the sample also records the heap after a forced one; `npm run start:profile` supplies that runtime together with a heap-snapshot signal, where `kill -USR2 <pid>` writes a snapshot. `/coredump [tag]` writes the same `.heapsnapshot` artifact into the client's working directory without a signal or a profiling runtime; the tag labels the file (default `snapshot`) and V8 serializes the heap synchronously, so the client pauses until the file exists. Open the snapshot in Chrome DevTools rather than the terminal. `--memory-log <path>` or `DSHT_MEMORY_LOG` changes the path, and `--no-memory-log` or `DSHT_MEMORY_LOG=off` disables it. The executable selects React's production build, because the development build writes one performance-timeline entry per rendered component that Node retains for the life of the process; set `DSHT_REACT_DEV=1` to keep the development build for React warnings and DevTools performance tracks. 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.
|
|
398
398
|
|
|
399
399
|
## Cost estimates
|
|
400
400
|
|
|
@@ -426,7 +426,7 @@ This repository publishes one public package, `@itookit/dsht`, from the `mushuan
|
|
|
426
426
|
|
|
427
427
|
| Field | Value |
|
|
428
428
|
| --- | --- |
|
|
429
|
-
| Name and version | `@itookit/dsht` `0.3.
|
|
429
|
+
| Name and version | `@itookit/dsht` `0.3.4` |
|
|
430
430
|
| Executable | `dsht`, or `npx @itookit/dsht` without installing |
|
|
431
431
|
| Library entries | `@itookit/dsht` and `@itookit/dsht/auth` |
|
|
432
432
|
| Author | lizlok@gmail.com |
|
package/README.zh.md
CHANGED
|
@@ -293,7 +293,7 @@ Enter 提交消息:代理处于 Working 时自动作为下一个 step 的转
|
|
|
293
293
|
|
|
294
294
|
鼠标滚轮和 Page Up/Down 滚动对话;滚到顶部自动加载更早的一页。查看旧记录时,新输出保留阅读位置;向下滚动即可恢复跟随最新输出。TUI 挂载时启用鼠标报告,退出时关闭,需要终端支持 SGR 鼠标报告。加载历史或搜索期间,Esc 或 Ctrl+C 优先取消本地操作,不中断远程任务。
|
|
295
295
|
|
|
296
|
-
在 `/ws` 和 `/resume`
|
|
296
|
+
在 `/ws` 和 `/resume` 列表中,每一行只报告三种用户可见状态,按最需要处理者在前排序:`?` needs you(本客户端持有该会话未回答的审批或提问)、`◐` working、`●` ready,其后是该会话最近活动时间。工作区行按同样顺序汇总其会话,不统计从未发过消息的会话;宽终端把状态写成文字并把目录放进右对齐列,窄终端只留 `?1 ◐2 ●6` 并在标题下打印标记图例。选中工作区或会话后,输入框为空时按 `d` 或 Delete 查看移除确认页。已有草稿时 `d` 仍正常输入,Backspace 不会打开移除页。`/ws --delete <名称或ID>` 和 `/resume --delete <标题或ID>` 打开相同确认页,`/resume --archive <标题或ID>` 也可归档会话。移除会话前重新读取 `session/list`;服务端明确标记 `blank: true`、未运行,且没有已知排队任务或本地正在提交的提示词时,直接归档,不再确认。判定使用服务端空会话标记,不依赖当前已加载历史或标题。其他会话仍需确认,默认选中取消,Esc 关闭确认页。工作区移除调用 `workspace/delete`,只移除注册,保留目录和会话。会话移除调用 `workspace/archiveSession`,从工作区会话列表和 `/resume all` 隐藏,但保留历史,可通过 `/resume ID` 重开。当前服务端 API 提供归档,没有永久删除会话接口。运行中的任务继续执行。归档当前会话会释放其 transcript 和排版缓存;操作被拒绝时保留列表及确认页,便于重试。
|
|
297
297
|
|
|
298
298
|
`/search` 对对话消息进行不区分大小写的字面文本匹配,包含旧页,排除纯工具行。搜索每次请求最多 80 条消息,扫描后释放临时页,只保留最多 200 条简短命中摘要,包含折叠的思考;结果截断时提示缩小查询范围。选择命中项只加载其序号附近的独立页面,`/latest` 释放该窗口。Esc 或 Ctrl+C 可取消搜索。稀有词或无匹配查询仍需通过 HTTP 扫描全部历史,此命令尚无服务端全文索引。`/history` 只列出已加载页面中自己的提示词,选择器显示的数字就是记录序号。`/ssearch` 与 `/wsearch` 调用 `session/search`,服务端搜索当前用户/助手消息内容,最多返回 20 个会话、摘要和截断标记,没有结果分页游标或命中记录序号。工作区筛选在全局数量限制之后进行,因此截断时可能漏掉工作区内的匹配会话;界面会提示结果不完整,可缩小查询范围。选择会话后加载其历史,再选择匹配消息跳转。所有操作均通过 HTTP 完成,不扫描服务端配置目录。
|
|
299
299
|
|
|
@@ -303,7 +303,7 @@ Enter 提交消息:代理处于 Working 时自动作为下一个 step 的转
|
|
|
303
303
|
|
|
304
304
|
鼠标复制时,先单击进入复制模式,待画面冻结后再拖动选择。松开鼠标不会恢复刷新,需按 Esc、Ctrl+S 或 Ctrl+C。终端原生 Shift+拖选可能不向应用发送鼠标事件,此时请先按 Ctrl+S。对话框中可按 Ctrl+S 冻结整个画面并释放鼠标捕获,再进行原生选择。
|
|
305
305
|
|
|
306
|
-
Tab 补全开头的 slash
|
|
306
|
+
Tab 补全开头的 slash 命令,多个候选时补到公共前缀。输入框支持 Readline 风格编辑,并保留粘贴内容中的换行与制表符;制表符按制表位显示、原样发送。单词以空白分隔;光标移动和逐字符删除保持完整的 Unicode 组合字符。输入框只显示少量内容行,超出后内部滚动并保持光标可见,因此长草稿不会把对话区挤没;窗口高度只由终端行数决定,终端更宽只会减少折行。行数超过该窗口的多行草稿会把中间行折叠为 `[N lines · X KB]`,首行与末行保持可见;←/→ 一次跨越整块,在其边界按 Backspace 或 Delete 删除整块,发送时仍为完整原文。空输入时 Ctrl+D 不退出;输入非空时 Ctrl+C 先清空输入,然后才停止或退出。未处理的修饰键快捷键不会将控制字符插入消息。终端退格键的 BS 和 DEL 编码均向后删除;独立 Delete 键(CSI 3~)向前删除。
|
|
307
307
|
|
|
308
308
|
| 按键 | 编辑操作 |
|
|
309
309
|
| --- | --- |
|
|
@@ -366,7 +366,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
366
366
|
|
|
367
367
|
审批提供 1. 允许一次、2. 拒绝、3. 停止当前轮次。输入框为空时,按 1–3 或 ↑/↓ 选择,再按 Enter 确认;初始不选中任何操作,Esc 清除高亮,请求重放时重新回到未选中。已有草稿时仍按正常文字输入处理,也保留 `/allow`、`/deny` 和 `/cancel` 命令。
|
|
368
368
|
|
|
369
|
-
用户问题显示题目进度、编号选项和说明。选择列表、待答问题、审批和文件补全与文本输入共用一个输入框边框。待答问题和审批会在输入框上方保留最近对话历史,历史视口使用剩余高度;对话框打开时减少上下留白,并在手动滚动或视口高度变化时刷新;可见选项数量按终端高度调整,并跟随当前高亮项滚动。输入框为空时,↑/↓ 或 1–9 定位选项,Enter 确认;数字键只选择、不提交。多选题用空格或 1–9 勾选/取消勾选,Enter 确认,超过九个选项仍可通过方向键访问。选择 Other answer
|
|
369
|
+
用户问题显示题目进度、编号选项和说明。选择列表、待答问题、审批和文件补全与文本输入共用一个输入框边框。待答问题和审批会在输入框上方保留最近对话历史,历史视口使用剩余高度;对话框打开时减少上下留白,并在手动滚动或视口高度变化时刷新;可见选项数量按终端高度调整,并跟随当前高亮项滚动。输入框为空时,↑/↓ 或 1–9 定位选项,Enter 确认;数字键只选择、不提交。多选题用空格或 1–9 勾选/取消勾选,Enter 确认,超过九个选项仍可通过方向键访问。选择 Other answer 后可输入纯数字自由文本,也保留普通文本回答。已有草稿时按正常文字输入处理;Esc 可退出提问:选项模式下放弃整组问题(服务端记为取消),Other 模式下第一次 Esc 只返回选项。全部题目回答完成后,一次提交结构化选项标签及可选自定义文本;失败时保留答案以便重试。已识别的提问和审批事件在本次连接中按事件 ID 保留,包括早于会话选择到达的重放事件;只展示当前会话对应的请求。切换列表不会退回这些请求,不识别的 waterfall 仍通过 `next` 委托后续处理。存活服务端在客户端重连后重发待答事件;客户端重启不保留尚未提交的回答草稿。正常退出 TUI 会取消正在运行的轮次。调用已取消/失败或服务端已重启时,无法靠本地 UI 状态恢复原等待,需要发送新提示词要求重新提问。提交失败时保留输入;HTTP 响应中断可能导致投递状态不确定,手动重发前应检查会话记录。客户端不会自动重试修改请求。
|
|
370
370
|
|
|
371
371
|
标题固定在可滚动对话区域上方,输入框和状态栏保留在下方。底部不再常驻快捷键说明,完整快捷键放在 `/help`,选择器只显示当前需要的导航提示。顶部单行优先显示最新会话标题(无标题时回退到 ID),宽屏时在其后附上工作区名称,不再显示主机地址和连接状态。未选择会话时显示工作区名称或 All workspaces,下方为分隔线;`/status` 保留完整会话 ID。取消回执在后续历史消息到达时保持可见,直到服务端报告空闲;接受取消不表示工具进程已经退出。
|
|
372
372
|
|
|
@@ -380,11 +380,11 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
380
380
|
|
|
381
381
|
## 实时状态
|
|
382
382
|
|
|
383
|
-
底栏分组显示 `◐ Working · 8s · Ctrl+C Stop
|
|
383
|
+
底栏分组显示 `◐ Working · 8s · Ctrl+C Stop`、`● Ready`,或本客户端还欠一个审批/回答时的 `? Needs you`、模型与思考强度、本会话费用与「今天花费(历史总计)」、十格上下文进度条及百分比、会话轮次与累计 token 及缓存命中率。宽终端为运行状态预留固定宽度,完成后模型和指标保持对齐。窄终端依次回收留白、隐藏进度条、缩短模型名、省略次要指标,优先保留停止提示。`/status` 显示主机 URL、操作状态、工作区完整路径、完整供应商/模型、下次模型、各项用量、轮次、队列、后台任务和四位小数费用。`!` 表示有指标或模型目录错误,或计费覆盖不完整;详情中显示原因。运行中使用最近实际使用的模型,空闲时使用下次模型,新会话使用服务端模型目录默认值。服务端设置、凭据和适配器变更通知会刷新模型目录。
|
|
384
384
|
|
|
385
385
|
工作计时使用已加载日志的 `turn/start` 时间戳。缺少该时间戳时,详情面板中的 `(observed)` 表示从客户端观察到运行开始计时;重连可能重置此备用计时。服务端报告空闲后停止计时。运行状态涵盖模型生成、工具执行及审批等待,不仅是文本输出。断线时明确标注为最后已知状态。展开面板把相关值合并到一行,并用单行状态栏已有的紧凑计数(`Context ~40% (400.6K/1M) · 229.7M tok`),因此 24 行终端可以一屏看到全部详情;`↑`/`↓` 逐行滚动,`PgUp`/`PgDn` 翻屏,页脚标出可见区间与总数。
|
|
386
386
|
|
|
387
|
-
单行状态栏按价值而不是按列来保留分组:`◐ 6:18 · bash 1:08 · ^C │ v4-flash · high · ctx 30% · S¥2.49* · ¥: 5.00 (12.34) · 2 turns · 34.5M tok · hit 92%`。当前阶段只报事实——推理时是 `think 28s`,工具运行时是工具名加已等待时长,回答流式输出时是 `write 12s`——绝不从静默推断异常,因为长时间推理与安静运行的工具都不是卡住。它显示的是**当前事件**的名字与年龄,并在下一段工作开始或回合关闭前保持不变:命令返回之后、下一次增量到达之前的静默期仍被算作这个回合的工作时间;`● Ready` 不显示阶段,因为只有宿主知道回合已经结束。两个费用分组互不替代:`S¥2.49*` 只报本会话,账本还没为它定价时如实写 `S?`;`¥: 5.00 (12.34)` 只报今天花费与历史总计。宽度不足时先丢价值最低的分组(缓存命中率、token、回合、effort、模型、当天花费,然后 ctx);本会话费用永不丢弃,只会挪到第二行;状态簇只在约二十列以下才让出阶段与停止提示。暂停的时钟会说明原因(`⏸ copy`、`⏸ dialog`、`⏸ history`)而不是无声冻结;`! Offline` 或 `⚠ Error`
|
|
387
|
+
单行状态栏按价值而不是按列来保留分组:`◐ 6:18 · bash 1:08 · ^C │ v4-flash · high · ctx 30% · S¥2.49* · ¥: 5.00 (12.34) · 2 turns · 34.5M tok · hit 92%`。当前阶段只报事实——推理时是 `think 28s`,工具运行时是工具名加已等待时长,回答流式输出时是 `write 12s`——绝不从静默推断异常,因为长时间推理与安静运行的工具都不是卡住。它显示的是**当前事件**的名字与年龄,并在下一段工作开始或回合关闭前保持不变:命令返回之后、下一次增量到达之前的静默期仍被算作这个回合的工作时间;`● Ready` 不显示阶段,因为只有宿主知道回合已经结束。两个费用分组互不替代:`S¥2.49*` 只报本会话,账本还没为它定价时如实写 `S?`;`¥: 5.00 (12.34)` 只报今天花费与历史总计。宽度不足时先丢价值最低的分组(缓存命中率、token、回合、effort、模型、当天花费,然后 ctx);本会话费用永不丢弃,只会挪到第二行;状态簇只在约二十列以下才让出阶段与停止提示。暂停的时钟会说明原因(`⏸ copy`、`⏸ dialog`、`⏸ history`)而不是无声冻结;`! Offline` 或 `⚠ Error` 会整体替换状态标记。本客户端还欠一个审批或回答时,`? Needs you` 会接管状态标记(对话框暂停时钟时也一样),因为需要动手的正是这个欠下的回答。
|
|
388
388
|
|
|
389
389
|
轮次数来自完整会话的 `sessionStats.turns` 投影。上下文占用标为 `~`:Harness 将供应商用量与对话变化估算值、最新模型容量结合。Token 总量来自完整会话的 `tokenUsage` 投影,分别显示非缓存输入、输出、缓存读取和缓存写入;思考 token 已包含在输出中。缓存命中率是缓存读取占三个互斥提示侧桶(非缓存输入、缓存读取、缓存写入)之和的比例,遇到部分命中时增加小数位而不是报成 `100%`。本会话与当天费用来自账本按会话保存的切片,因此还没有切片的会话显示为未知,而不是当天的花费。总量随服务端用量投影更新,不按流式字符计数。缺失数据显示 `unknown` 或 `?`。重连时控制流基线整体替换状态,每个投影键的序号防止旧 follow 快照覆盖较新的指标。
|
|
390
390
|
|
|
@@ -426,7 +426,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
426
426
|
|
|
427
427
|
| 字段 | 值 |
|
|
428
428
|
| --- | --- |
|
|
429
|
-
| 名称与版本 | `@itookit/dsht` `0.3.
|
|
429
|
+
| 名称与版本 | `@itookit/dsht` `0.3.4` |
|
|
430
430
|
| 可执行命令 | `dsht`,不安装时用 `npx @itookit/dsht` |
|
|
431
431
|
| 库入口 | `@itookit/dsht` 和 `@itookit/dsht/auth` |
|
|
432
432
|
| 作者 | lizlok\@gmail.com |
|
package/dist/cli/dsht.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Standalone executable entry; connects to an existing host and never launches Harness. */
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { CostLedger, loadPrices } from "../cost/index.js";
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { mount } from "../ui/mount.js";
|
|
9
|
+
import { ensureDirectory } from "../storage/index.js";
|
|
10
|
+
import { sessionLabel } from "../session/navigation.js";
|
|
11
|
+
import { CookieStore, login } from "../transport/auth.js";
|
|
12
|
+
import { Client } from "../transport/client.js";
|
|
13
|
+
import { historyLimits } from "../session/memory.js";
|
|
14
|
+
import { Controller } from "../controller/controller.js";
|
|
15
|
+
import { endpoint } from "../transport/endpoint.js";
|
|
16
|
+
import { errorText, safeText, string } from "../transport/wire.js";
|
|
17
|
+
const HELP = `Usage: dsht [options] [list workspaces|list sessions]
|
|
18
|
+
|
|
19
|
+
With no command, choose a workspace and session interactively.
|
|
20
|
+
|
|
21
|
+
--url <url> Host URL, or the dsh web URL with ?token= (DSH_URL)
|
|
22
|
+
--workspace <id> Filter list sessions by workspace
|
|
23
|
+
--session <id> Open a session directly
|
|
24
|
+
--auth-dir <path> Private cookie directory (or DSHT_AUTH_DIR)
|
|
25
|
+
--history-records <n> Soft history record limit (default 2000)
|
|
26
|
+
--history-mb <n> Soft history payload budget in MiB (default 16)
|
|
27
|
+
--memory-log <path> Append runtime memory samples; a failing log stops itself
|
|
28
|
+
--no-memory-log Disable the runtime memory log (default: enabled)
|
|
29
|
+
--json Print machine-readable list output
|
|
30
|
+
--help Show this help
|
|
31
|
+
|
|
32
|
+
The default host is http://127.0.0.1:3080.
|
|
33
|
+
First login: export DSH_TOKEN, or export DSH_URL as the URL printed by dsh web.
|
|
34
|
+
Cookies are saved per server origin and reused on later starts. Tokens are never saved.
|
|
35
|
+
/cost shows the session and today CNY estimates.
|
|
36
|
+
DSHT_CONFIG_DIR overrides the prices.json directory; DSHT_STATE_DIR overrides usage storage.
|
|
37
|
+
The memory log defaults to <state>/memory.log; DSHT_MEMORY_LOG sets another path or 'off'.
|
|
38
|
+
prices.json overrides the shipped rates and is seeded on first use; every scan re-decides the
|
|
39
|
+
history with the table loaded then, so an edited table reaches past requests on the next scan.
|
|
40
|
+
Examples:
|
|
41
|
+
npx @itookit/dsht
|
|
42
|
+
dsht list workspaces --json
|
|
43
|
+
dsht list sessions --workspace <id> --json
|
|
44
|
+
`;
|
|
45
|
+
async function main() {
|
|
46
|
+
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
47
|
+
url: { type: 'string', default: process.env.DSH_URL ?? 'http://127.0.0.1:3080' },
|
|
48
|
+
'history-records': { type: 'string' }, 'history-mb': { type: 'string' },
|
|
49
|
+
workspace: { type: 'string' }, session: { type: 'string' }, 'auth-dir': { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean' },
|
|
50
|
+
'memory-log': { type: 'string' }, 'no-memory-log': { type: 'boolean' },
|
|
51
|
+
} });
|
|
52
|
+
if (values.help) {
|
|
53
|
+
process.stdout.write(HELP);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const list = positionals[0] === 'list' && ['workspaces', 'sessions'].includes(positionals[1] ?? '') && positionals.length === 2;
|
|
57
|
+
if (positionals.length && !list)
|
|
58
|
+
throw new Error('Unknown command. Use --help.');
|
|
59
|
+
if (!list && (values.json || values.workspace))
|
|
60
|
+
throw new Error('--json and --workspace apply to list commands');
|
|
61
|
+
if (list && values.session)
|
|
62
|
+
throw new Error('--session applies to interactive mode');
|
|
63
|
+
const limits = historyLimits(values['history-records'], values['history-mb']);
|
|
64
|
+
const { url, token } = endpoint(values.url, process.env.DSH_TOKEN);
|
|
65
|
+
const store = new CookieStore(values['auth-dir']);
|
|
66
|
+
if (list) {
|
|
67
|
+
const client = new Client(url);
|
|
68
|
+
try {
|
|
69
|
+
await login(client, token, store);
|
|
70
|
+
if (positionals[1] === 'workspaces' || values.workspace)
|
|
71
|
+
await client.connect();
|
|
72
|
+
const items = positionals[1] === 'workspaces' ? await client.listWorkspaces() : await client.listSessions(values.workspace);
|
|
73
|
+
if (values.json)
|
|
74
|
+
process.stdout.write(`${JSON.stringify({ items }, null, 2)}\n`);
|
|
75
|
+
else {
|
|
76
|
+
const lines = items.map(item => positionals[1] === 'workspaces'
|
|
77
|
+
? `${string(item.workspaceId)}\t${string(item.title)}\t${string(item.path)}`
|
|
78
|
+
: `${string(item.sessionId)}\t${sessionLabel(item)}\t${item.running ? 'running' : 'idle'}`);
|
|
79
|
+
process.stdout.write(`${safeText(lines.join('\n'))}${lines.length ? '\n' : ''}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await client.close();
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const config = process.env.DSHT_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'dsht');
|
|
88
|
+
await ensureDirectory(config);
|
|
89
|
+
const { prices, custom } = await loadPrices(config);
|
|
90
|
+
const stateRoot = process.env.DSHT_STATE_DIR ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht');
|
|
91
|
+
const costDirectory = join(stateRoot, 'cost', createHash('sha256').update(new URL(url).origin).digest('hex'));
|
|
92
|
+
const costs = new CostLedger(prices, costDirectory, custom);
|
|
93
|
+
await costs.load();
|
|
94
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
95
|
+
throw new Error('Interactive mode requires a terminal. Use list workspaces or list sessions for scripts.');
|
|
96
|
+
const controller = new Controller(url, token, values.session, undefined, client => login(client, token, store), costs, limits, memoryLogPath(stateRoot, values['memory-log'], values['no-memory-log']));
|
|
97
|
+
const app = mount(controller);
|
|
98
|
+
const terminate = () => app.unmount();
|
|
99
|
+
process.once('SIGTERM', terminate);
|
|
100
|
+
controller.start();
|
|
101
|
+
try {
|
|
102
|
+
await app.waitUntilExit();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
process.off('SIGTERM', terminate);
|
|
106
|
+
await controller.shutdown();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Resolve the runtime memory log path: an explicit flag wins, then the environment, then the default.
|
|
110
|
+
* @param stateRoot - Application state root used for the default path.
|
|
111
|
+
* @param requested - `--memory-log` value, when given.
|
|
112
|
+
* @param disabled - `--no-memory-log` flag.
|
|
113
|
+
* @returns Absolute log path, or undefined when the log is disabled.
|
|
114
|
+
*/
|
|
115
|
+
function memoryLogPath(stateRoot, requested, disabled) {
|
|
116
|
+
if (requested !== undefined && requested.trim() === '')
|
|
117
|
+
throw new Error('--memory-log requires a path');
|
|
118
|
+
if (disabled)
|
|
119
|
+
return undefined;
|
|
120
|
+
const chosen = (requested ?? process.env.DSHT_MEMORY_LOG)?.trim();
|
|
121
|
+
if (chosen === undefined || chosen === '')
|
|
122
|
+
return join(stateRoot, 'memory.log');
|
|
123
|
+
return chosen === 'off' ? undefined : chosen;
|
|
124
|
+
}
|
|
125
|
+
main().catch(error => { process.stderr.write(`${errorText(error)}\n`); process.exitCode = 1; });
|
package/dist/cli/index.js
CHANGED
|
@@ -1,125 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { Controller } from "../controller/controller.js";
|
|
15
|
-
import { endpoint } from "../transport/endpoint.js";
|
|
16
|
-
import { errorText, safeText, string } from "../transport/wire.js";
|
|
17
|
-
const HELP = `Usage: dsht [options] [list workspaces|list sessions]
|
|
18
|
-
|
|
19
|
-
With no command, choose a workspace and session interactively.
|
|
20
|
-
|
|
21
|
-
--url <url> Host URL, or the dsh web URL with ?token= (DSH_URL)
|
|
22
|
-
--workspace <id> Filter list sessions by workspace
|
|
23
|
-
--session <id> Open a session directly
|
|
24
|
-
--auth-dir <path> Private cookie directory (or DSHT_AUTH_DIR)
|
|
25
|
-
--history-records <n> Soft history record limit (default 2000)
|
|
26
|
-
--history-mb <n> Soft history payload budget in MiB (default 16)
|
|
27
|
-
--memory-log <path> Append runtime memory samples; a failing log stops itself
|
|
28
|
-
--no-memory-log Disable the runtime memory log (default: enabled)
|
|
29
|
-
--json Print machine-readable list output
|
|
30
|
-
--help Show this help
|
|
31
|
-
|
|
32
|
-
The default host is http://127.0.0.1:3080.
|
|
33
|
-
First login: export DSH_TOKEN, or export DSH_URL as the URL printed by dsh web.
|
|
34
|
-
Cookies are saved per server origin and reused on later starts. Tokens are never saved.
|
|
35
|
-
/cost shows the session and today CNY estimates.
|
|
36
|
-
DSHT_CONFIG_DIR overrides the prices.json directory; DSHT_STATE_DIR overrides usage storage.
|
|
37
|
-
The memory log defaults to <state>/memory.log; DSHT_MEMORY_LOG sets another path or 'off'.
|
|
38
|
-
prices.json overrides the shipped rates and is seeded on first use; every scan re-decides the
|
|
39
|
-
history with the table loaded then, so an edited table reaches past requests on the next scan.
|
|
40
|
-
Examples:
|
|
41
|
-
npx @itookit/dsht
|
|
42
|
-
dsht list workspaces --json
|
|
43
|
-
dsht list sessions --workspace <id> --json
|
|
44
|
-
`;
|
|
45
|
-
async function main() {
|
|
46
|
-
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
47
|
-
url: { type: 'string', default: process.env.DSH_URL ?? 'http://127.0.0.1:3080' },
|
|
48
|
-
'history-records': { type: 'string' }, 'history-mb': { type: 'string' },
|
|
49
|
-
workspace: { type: 'string' }, session: { type: 'string' }, 'auth-dir': { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean' },
|
|
50
|
-
'memory-log': { type: 'string' }, 'no-memory-log': { type: 'boolean' },
|
|
51
|
-
} });
|
|
52
|
-
if (values.help) {
|
|
53
|
-
process.stdout.write(HELP);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const list = positionals[0] === 'list' && ['workspaces', 'sessions'].includes(positionals[1] ?? '') && positionals.length === 2;
|
|
57
|
-
if (positionals.length && !list)
|
|
58
|
-
throw new Error('Unknown command. Use --help.');
|
|
59
|
-
if (!list && (values.json || values.workspace))
|
|
60
|
-
throw new Error('--json and --workspace apply to list commands');
|
|
61
|
-
if (list && values.session)
|
|
62
|
-
throw new Error('--session applies to interactive mode');
|
|
63
|
-
const limits = historyLimits(values['history-records'], values['history-mb']);
|
|
64
|
-
const { url, token } = endpoint(values.url, process.env.DSH_TOKEN);
|
|
65
|
-
const store = new CookieStore(values['auth-dir']);
|
|
66
|
-
if (list) {
|
|
67
|
-
const client = new Client(url);
|
|
68
|
-
try {
|
|
69
|
-
await login(client, token, store);
|
|
70
|
-
if (positionals[1] === 'workspaces' || values.workspace)
|
|
71
|
-
await client.connect();
|
|
72
|
-
const items = positionals[1] === 'workspaces' ? await client.listWorkspaces() : await client.listSessions(values.workspace);
|
|
73
|
-
if (values.json)
|
|
74
|
-
process.stdout.write(`${JSON.stringify({ items }, null, 2)}\n`);
|
|
75
|
-
else {
|
|
76
|
-
const lines = items.map(item => positionals[1] === 'workspaces'
|
|
77
|
-
? `${string(item.workspaceId)}\t${string(item.title)}\t${string(item.path)}`
|
|
78
|
-
: `${string(item.sessionId)}\t${sessionLabel(item)}\t${item.running ? 'running' : 'idle'}`);
|
|
79
|
-
process.stdout.write(`${safeText(lines.join('\n'))}${lines.length ? '\n' : ''}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
finally {
|
|
83
|
-
await client.close();
|
|
84
|
-
}
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const config = process.env.DSHT_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'dsht');
|
|
88
|
-
await ensureDirectory(config);
|
|
89
|
-
const { prices, custom } = await loadPrices(config);
|
|
90
|
-
const stateRoot = process.env.DSHT_STATE_DIR ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht');
|
|
91
|
-
const costDirectory = join(stateRoot, 'cost', createHash('sha256').update(new URL(url).origin).digest('hex'));
|
|
92
|
-
const costs = new CostLedger(prices, costDirectory, custom);
|
|
93
|
-
await costs.load();
|
|
94
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
95
|
-
throw new Error('Interactive mode requires a terminal. Use list workspaces or list sessions for scripts.');
|
|
96
|
-
const controller = new Controller(url, token, values.session, undefined, client => login(client, token, store), costs, limits, memoryLogPath(stateRoot, values['memory-log'], values['no-memory-log']));
|
|
97
|
-
const app = mount(controller);
|
|
98
|
-
const terminate = () => app.unmount();
|
|
99
|
-
process.once('SIGTERM', terminate);
|
|
100
|
-
controller.start();
|
|
101
|
-
try {
|
|
102
|
-
await app.waitUntilExit();
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
process.off('SIGTERM', terminate);
|
|
106
|
-
await controller.shutdown();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/** Resolve the runtime memory log path: an explicit flag wins, then the environment, then the default.
|
|
110
|
-
* @param stateRoot - Application state root used for the default path.
|
|
111
|
-
* @param requested - `--memory-log` value, when given.
|
|
112
|
-
* @param disabled - `--no-memory-log` flag.
|
|
113
|
-
* @returns Absolute log path, or undefined when the log is disabled.
|
|
2
|
+
/**
|
|
3
|
+
* Executable entry. It must set the React build before anything imports Ink.
|
|
4
|
+
*
|
|
5
|
+
* A static `import` is evaluated before this file's body runs, so importing the CLI
|
|
6
|
+
* directly would let `react-reconciler` resolve to its development build and emit a
|
|
7
|
+
* `performance.measure()` entry for every render. Node's performance timeline holds
|
|
8
|
+
* those entries for the life of the process and never trims them, which grew the heap
|
|
9
|
+
* by roughly 1.2 KB per render until the process was restarted. The dynamic import
|
|
10
|
+
* below is therefore load-bearing: `NODE_ENV` has to be set first.
|
|
11
|
+
*
|
|
12
|
+
* Set `DSHT_REACT_DEV=1` to keep the development build for React warnings and
|
|
13
|
+
* DevTools performance tracks.
|
|
114
14
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return undefined;
|
|
120
|
-
const chosen = (requested ?? process.env.DSHT_MEMORY_LOG)?.trim();
|
|
121
|
-
if (chosen === undefined || chosen === '')
|
|
122
|
-
return join(stateRoot, 'memory.log');
|
|
123
|
-
return chosen === 'off' ? undefined : chosen;
|
|
124
|
-
}
|
|
125
|
-
main().catch(error => { process.stderr.write(`${errorText(error)}\n`); process.exitCode = 1; });
|
|
15
|
+
if ((process.env.DSHT_REACT_DEV ?? '') === '')
|
|
16
|
+
process.env.NODE_ENV ??= 'production';
|
|
17
|
+
await import('./dsht.js');
|
|
18
|
+
export {};
|
|
@@ -75,6 +75,10 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
75
75
|
* diagram cache — and the work the last cost scan re-read, which is the only timer here whose
|
|
76
76
|
* per-pass work scales with history. With a runtime that exposes `gc`, the sample also reports
|
|
77
77
|
* the heap after a forced collection, so retained state and uncollected garbage stay distinct.
|
|
78
|
+
*
|
|
79
|
+
* React's development build appends one performance-timeline entry per rendered component and Node
|
|
80
|
+
* never trims them, so the sample counts them and then bounds them; `perfMeasuresCleared` separates
|
|
81
|
+
* entries this sample released from entries the build created since the last one.
|
|
78
82
|
*/
|
|
79
83
|
private memorySample;
|
|
80
84
|
/** Collect before reading the heap when the runtime exposes a collection.
|
|
@@ -120,6 +124,8 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
120
124
|
get workingSince(): number | undefined;
|
|
121
125
|
/** @returns Sessions accounted to the selected workspace, minus archived identities. */
|
|
122
126
|
get visibleSessions(): ObjectValue[];
|
|
127
|
+
/** @returns Unanswered interactions by session, for the state each list row reports. */
|
|
128
|
+
pendingCounts(): ReadonlyMap<string, number>;
|
|
123
129
|
/** Load the optional preset roster once per connection. */
|
|
124
130
|
loadPresetNames(): void;
|
|
125
131
|
/** @returns Host model routes and adapter-owned reasoning choices. */
|
|
@@ -264,6 +270,8 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
264
270
|
* @param allowed - Whether the request is approved once.
|
|
265
271
|
*/
|
|
266
272
|
approve(allowed: boolean): Promise<void>;
|
|
273
|
+
/** Dismiss the whole pending question set without answering it, as the Web close button does. */
|
|
274
|
+
dismissQuestion(): Promise<void>;
|
|
267
275
|
}
|
|
268
276
|
export type { HistorySearch, RemovalTarget } from '../session/types.ts';
|
|
269
277
|
export type { State } from '../state.ts';
|
|
@@ -10,6 +10,7 @@ import { CatalogController } from "../catalog/controller.js";
|
|
|
10
10
|
import { CostController } from "../cost/controller.js";
|
|
11
11
|
import { ConnectionController } from "./connection.js";
|
|
12
12
|
import { MemoryLog } from "./memory-log.js";
|
|
13
|
+
import { clearReactMeasures, measureCount } from "./perf-measures.js";
|
|
13
14
|
import { initialState } from "../state.js";
|
|
14
15
|
/** Application facade over the domain controllers; the UI owns only this object.
|
|
15
16
|
*
|
|
@@ -129,6 +130,10 @@ export class Controller {
|
|
|
129
130
|
* diagram cache — and the work the last cost scan re-read, which is the only timer here whose
|
|
130
131
|
* per-pass work scales with history. With a runtime that exposes `gc`, the sample also reports
|
|
131
132
|
* the heap after a forced collection, so retained state and uncollected garbage stay distinct.
|
|
133
|
+
*
|
|
134
|
+
* React's development build appends one performance-timeline entry per rendered component and Node
|
|
135
|
+
* never trims them, so the sample counts them and then bounds them; `perfMeasuresCleared` separates
|
|
136
|
+
* entries this sample released from entries the build created since the last one.
|
|
132
137
|
*/
|
|
133
138
|
memorySample() {
|
|
134
139
|
const memory = process.memoryUsage();
|
|
@@ -136,11 +141,14 @@ export class Controller {
|
|
|
136
141
|
const ledger = this.costs?.summary();
|
|
137
142
|
const layout = layoutStats(transcript);
|
|
138
143
|
const markdown = markdownCacheStats();
|
|
144
|
+
const measuresCleared = clearReactMeasures();
|
|
145
|
+
const measures = measureCount();
|
|
139
146
|
const gc = this.forcedGc();
|
|
140
147
|
return {
|
|
141
148
|
time: new Date().toISOString(),
|
|
142
149
|
rss: memory.rss, heapTotal: memory.heapTotal, heapUsed: memory.heapUsed,
|
|
143
150
|
external: memory.external, arrayBuffers: memory.arrayBuffers,
|
|
151
|
+
...(measures === undefined ? {} : { perfMeasures: measures, perfMeasuresCleared: measuresCleared }),
|
|
144
152
|
...(gc === undefined ? {} : { heapUsedAfterGc: gc.used, gcMs: gc.ms }),
|
|
145
153
|
online: this.state.online, screen: this.state.screen,
|
|
146
154
|
session: this.state.sessionId ?? null,
|
|
@@ -224,6 +232,8 @@ export class Controller {
|
|
|
224
232
|
get workingSince() { return this.session.workingSince; }
|
|
225
233
|
/** @returns Sessions accounted to the selected workspace, minus archived identities. */
|
|
226
234
|
get visibleSessions() { return this.session.visibleSessions; }
|
|
235
|
+
/** @returns Unanswered interactions by session, for the state each list row reports. */
|
|
236
|
+
pendingCounts() { return this.session.pendingCounts(); }
|
|
227
237
|
/** Load the optional preset roster once per connection. */
|
|
228
238
|
loadPresetNames() { this.catalog.loadPresetNames(); }
|
|
229
239
|
/** @returns Host model routes and adapter-owned reasoning choices. */
|
|
@@ -369,4 +379,6 @@ export class Controller {
|
|
|
369
379
|
* @param allowed - Whether the request is approved once.
|
|
370
380
|
*/
|
|
371
381
|
async approve(allowed) { await this.session.approve(allowed); }
|
|
382
|
+
/** Dismiss the whole pending question set without answering it, as the Web close button does. */
|
|
383
|
+
async dismissQuestion() { await this.session.dismissQuestion(); }
|
|
372
384
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Drop the render measurements React's development build appends to the global performance timeline.
|
|
2
|
+
*
|
|
3
|
+
* `react-reconciler` emits one `performance.measure()` entry per rendered component when it resolves
|
|
4
|
+
* to its development build, and Node keeps every entry for the life of the process because
|
|
5
|
+
* `performance.clearMeasures()` is the only way to release them. Each entry also carries a
|
|
6
|
+
* `detail.devtools.properties` payload describing the component's props, which is where the memory
|
|
7
|
+
* actually goes: roughly 1.2 KB per render once the duplicated property names are counted.
|
|
8
|
+
*
|
|
9
|
+
* The production build emits no measurements at all, so `src/cli/index.ts` selects it and this
|
|
10
|
+
* module is a safety net for `DSHT_REACT_DEV=1`, where the development build is deliberate.
|
|
11
|
+
*/
|
|
12
|
+
/** Measure names the current timeline holds that React created.
|
|
13
|
+
*
|
|
14
|
+
* A name only counts when it is exactly one of `REACT_NAMES` or carries React's zero-width prefix,
|
|
15
|
+
* so entries an application or a library created under its own name are never selected.
|
|
16
|
+
* @param performance - Timeline to read.
|
|
17
|
+
* @returns Names React created, without duplicates.
|
|
18
|
+
*/
|
|
19
|
+
export declare function reactMeasureNames(performance: Performance): string[];
|
|
20
|
+
/** Remove React's render measurements, and only those, from the global performance timeline.
|
|
21
|
+
*
|
|
22
|
+
* Intended to run on the memory-sampling interval: the development build keeps appending, so one
|
|
23
|
+
* pass only bounds the total instead of ending it. A failure is not worth propagating, because this
|
|
24
|
+
* is a bounded diagnostic and a measurement library that rejects input should not stop the client.
|
|
25
|
+
* @returns How many distinct measure names were cleared.
|
|
26
|
+
*/
|
|
27
|
+
export declare function clearReactMeasures(): number;
|
|
28
|
+
/** How many measure entries the timeline currently holds.
|
|
29
|
+
*
|
|
30
|
+
* Recorded next to the heap counters so a memory log shows whether retained growth tracks the
|
|
31
|
+
* render count. Entries cleared by `clearReactMeasures` leave the count at zero.
|
|
32
|
+
* @returns Entry count, or undefined on a runtime without a readable timeline.
|
|
33
|
+
*/
|
|
34
|
+
export declare function measureCount(): number | undefined;
|