@itookit/dsht 0.3.2 → 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 +21 -20
- package/README.zh.md +21 -20
- package/dist/cli/dsht.d.ts +2 -0
- package/dist/cli/dsht.js +125 -0
- package/dist/cli/index.js +16 -126
- package/dist/controller/controller.d.ts +18 -1
- package/dist/controller/controller.js +24 -2
- package/dist/controller/perf-measures.d.ts +34 -0
- package/dist/controller/perf-measures.js +78 -0
- package/dist/cost/config.d.ts +17 -0
- package/dist/cost/config.js +68 -0
- package/dist/cost/controller.d.ts +9 -2
- package/dist/cost/controller.js +10 -2
- package/dist/cost/index.d.ts +5 -4
- package/dist/cost/index.js +4 -3
- package/dist/cost/ledger-files.d.ts +4 -8
- package/dist/cost/ledger-files.js +46 -71
- package/dist/cost/ledger.d.ts +33 -20
- package/dist/cost/ledger.js +78 -68
- package/dist/cost/pricing.d.ts +55 -17
- package/dist/cost/pricing.js +126 -44
- package/dist/cost/records.js +18 -5
- package/dist/cost/types.d.ts +34 -18
- package/dist/cost/types.js +4 -0
- package/dist/session/controller.d.ts +16 -0
- package/dist/session/controller.js +33 -0
- package/dist/session/navigation.d.ts +80 -0
- package/dist/session/navigation.js +107 -0
- package/dist/session/transcript.d.ts +48 -6
- package/dist/session/transcript.js +117 -14
- package/dist/storage/files.d.ts +8 -0
- package/dist/storage/files.js +17 -0
- package/dist/storage/heap-snapshot.d.ts +19 -0
- package/dist/storage/heap-snapshot.js +29 -0
- package/dist/storage/index.d.ts +2 -1
- package/dist/storage/index.js +2 -1
- package/dist/ui/app.js +116 -10
- package/dist/ui/chat/history-view.d.ts +5 -1
- package/dist/ui/chat/history-view.js +9 -4
- package/dist/ui/chat/status.d.ts +16 -4
- package/dist/ui/chat/status.js +112 -32
- package/dist/ui/commands/parse.d.ts +3 -0
- package/dist/ui/commands/parse.js +5 -0
- package/dist/ui/commands/registry.js +1 -0
- package/dist/ui/dialogs/cost.js +3 -3
- 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
|
@@ -47,7 +47,7 @@ Main features:
|
|
|
47
47
|
- Queued prompts, steering, turn cancellation, approvals, and free-text question answers.
|
|
48
48
|
- Cookie persistence per host, automatic reconnect, and snapshot replacement, so control survives a dropped connection.
|
|
49
49
|
- JSON or tab-separated workspace/session lists for scripts, plus a reusable HTTP client.
|
|
50
|
-
- **Cost-aware**: session
|
|
50
|
+
- **Cost-aware**: session and today CNY estimates with versioned peak/off-peak prices and `/cost` summaries.
|
|
51
51
|
|
|
52
52
|
## Why use dsht?
|
|
53
53
|
|
|
@@ -166,7 +166,7 @@ npm start
|
|
|
166
166
|
|
|
167
167
|
Both paths read the same `DSH_URL` and `DSH_TOKEN` variables.
|
|
168
168
|
|
|
169
|
-
Select a workspace with ↑/↓ and Enter, then select a session or **New session**. **All sessions** also exposes sessions outside registered workspaces. **Add workspace**
|
|
169
|
+
Select a workspace with ↑/↓ and Enter, then select a session or **New session**. **All sessions** also exposes sessions outside registered workspaces. **Add workspace (this directory)** registers the directory `dsht` itself runs in, and appears only while the host does not already have it; **Add workspace (host directory)** takes an existing absolute directory on the host, which may differ from your local filesystem, and Esc leaves that prompt for the picker again. Creating a session requires a selected workspace.
|
|
170
170
|
|
|
171
171
|
On first login, authentication exchanges the token at `GET /` and saves the cookie per HTTP origin. Later starts, including list commands, reuse that cookie without a token. The store uses `$XDG_STATE_HOME/dsht/auth`, or `~/.local/state/dsht/auth` when unset; `--auth-dir` or `DSHT_AUTH_DIR` overrides it. POSIX directories use 0700 and cookie files use 0600; Windows uses the account directory's inherited access controls. Launch tokens are never saved.
|
|
172
172
|
|
|
@@ -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
|
| --- | --- |
|
|
@@ -319,7 +319,7 @@ Tab completes the leading slash command, extending an ambiguous draft to the sha
|
|
|
319
319
|
|
|
320
320
|
| Command | Action |
|
|
321
321
|
| --- | --- |
|
|
322
|
-
| `/ws` | Show all workspaces; choosing one opens its session list |
|
|
322
|
+
| `/ws` | Show all workspaces and both add-workspace entries; choosing one opens its session list |
|
|
323
323
|
| `/ws TARGET` | Select a workspace by ID, exact name/path, or unique ID prefix |
|
|
324
324
|
| `/resume` | Show sessions in the current workspace; choose a workspace first if none is selected |
|
|
325
325
|
| `/resume TARGET` | Open a session by ID, exact title, or unique ID prefix across workspaces |
|
|
@@ -335,6 +335,7 @@ Tab completes the leading slash command, extending an ambiguous draft to the sha
|
|
|
335
335
|
| `/feedback TEXT` | Record feedback about the current session |
|
|
336
336
|
| `/export [local.zip]` | Download the session log ZIP to a new local file |
|
|
337
337
|
| `/export-html [local.html]` | Save the loaded conversation as offline HTML with diagrams and math |
|
|
338
|
+
| `/coredump [tag]` | Write a V8 heap snapshot to the working directory for memory diagnosis |
|
|
338
339
|
| `/older` | Load older history |
|
|
339
340
|
| `/history [text]` | List your own prompts, optionally filtered; Enter jumps to the selected record |
|
|
340
341
|
| `/search <text>` | Search history page by page; choose a match to open its location |
|
|
@@ -344,12 +345,12 @@ Tab completes the leading slash command, extending an ambiguous draft to the sha
|
|
|
344
345
|
| `/wsearch <text>` | Search sessions across all workspaces visible to the host |
|
|
345
346
|
| `/allow`, `/deny` | Answer the displayed approval; allow applies once |
|
|
346
347
|
| `/status` | Expand or collapse full footer details; ↑/↓ scroll it, PgUp/PgDn page |
|
|
347
|
-
| `/cost` | Toggle session
|
|
348
|
+
| `/cost` | Toggle session and today estimates and refresh usage |
|
|
348
349
|
| `/think` | List reasoning with user prompt summaries; ↑/↓ and Enter jump to and expand a thought |
|
|
349
350
|
| `/think SEQ` | Toggle one loaded thought; `live` toggles the active attempt |
|
|
350
351
|
| `/help`, `/quit` | List every command with its description, or exit |
|
|
351
352
|
|
|
352
|
-
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.
|
|
353
|
+
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; `/history` also expires after ten seconds, so a forgotten lookup releases the composer. 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.
|
|
353
354
|
|
|
354
355
|
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.
|
|
355
356
|
|
|
@@ -365,7 +366,7 @@ Below 62 terminal columns, active streaming reasoning defaults to one folded row
|
|
|
365
366
|
|
|
366
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.
|
|
367
368
|
|
|
368
|
-
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.
|
|
369
370
|
|
|
370
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.
|
|
371
372
|
|
|
@@ -379,13 +380,13 @@ Closed `mermaid` fences render as Unicode diagrams for supported flowcharts, sta
|
|
|
379
380
|
|
|
380
381
|
## Live status
|
|
381
382
|
|
|
382
|
-
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.
|
|
383
384
|
|
|
384
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.
|
|
385
386
|
|
|
386
|
-
The single-row bar keeps its groups by value rather than by column: `◐ 6:18 · bash 1:08 · ^C │
|
|
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.
|
|
387
388
|
|
|
388
|
-
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. 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.
|
|
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.
|
|
389
390
|
|
|
390
391
|
The default [Catppuccin Mocha](https://catppuccin.com/palette/) theme distinguishes `❯ User` (blue), `✦ Assistant` (green), reasoning (mauve), tools (sky), success (green), and errors (red). The compact status bar uses green for Ready, yellow for Working, red for offline, subdued gray for a paused reason and the separators, mauve for model/effort, sky for costs, and subdued gray for usage. Context occupancy changes from green to yellow at 80% and red at 95%; these are visual thresholds, not host compaction triggers. Groups are fitted by priority before ANSI styling, so a plain terminal shows the same text. Semantic colors live in `src/ui/theme/index.ts`; the application accepts a theme independently of stored messages. Ink adapts ANSI output to terminal capabilities, and plain terminals retain the role markers. Tool calls show their name and description plus a `$` preview of the command's first line when it differs from the description. Without a description, the first command line, path, or query supplies the summary. Each line truncates by terminal display width; completion updates the original call from ⚙ to ✓ or ✗ by call ID, without a second result entry. Command previews retain two-space indentation. Results whose calls are outside the loaded window remain visible until their call page is loaded; nested result bodies stay hidden.
|
|
391
392
|
|
|
@@ -393,7 +394,7 @@ Reasoning streams in full while being generated, then folds when its block close
|
|
|
393
394
|
|
|
394
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.
|
|
395
396
|
|
|
396
|
-
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. `--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.
|
|
397
398
|
|
|
398
399
|
## Cost estimates
|
|
399
400
|
|
|
@@ -401,17 +402,17 @@ History separates semantic message blocks, prompt/reasoning summaries, view-only
|
|
|
401
402
|
|
|
402
403
|
> These figures are a high-precision estimate from Harness-visible usage and local price configuration, for cost monitoring and control. They are not a provider account bill, and the provider invoice remains authoritative.
|
|
403
404
|
|
|
404
|
-
`/cost` shows the selected session
|
|
405
|
+
`/cost` shows the selected session and today. Dates use Asia/Shanghai. The status bar shows this session's cost with today's total in parentheses (`¥: 1.23(113.00)`); the pair is not a budget. `*` marks a subtotal that is not exact, because a request carries no timestamp, no price covers it, or the scan has not covered every session yet; `/cost` names the reason. Each host origin has a separate ledger. Totals cover HTTP-visible sessions and previously cached sessions; they are not account-wide provider bills.
|
|
405
406
|
|
|
406
|
-
The client reads complete histories in the background on connection, every 60 seconds, at turn completion, and when opening `/cost`.
|
|
407
|
+
The client reads complete histories in the background on connection, every 60 seconds, at turn completion, and when opening `/cost`. Every new connection re-reads every session once, because the host keeps running while this client is disconnected and a recorded update time cannot show what happened during the gap; within one connection, idle sessions whose host update timestamp has not moved are skipped. No model requests are made by billing. Esc or Ctrl+C cancels an explicit refresh. The ledger counts disjoint uncached input, cache read/write and output buckets; reasoning is already part of output. Retries count separately, replacement samples update their attempt, and fork-inherited history is excluded. A request that reports no usable usage, no settlement timestamp, or a cache-write bucket the official table does not price is counted as unpriced instead of being guessed at, and a model no entry covers stays unpriced; an unlisted provider is never billed from the official table. Every listed session is read independently, so one unreachable or rejected session is reported as a failure count instead of stopping the scan, and a subagent child is read under its durable parent address. Failed scans retain labelled partial cached totals.
|
|
407
408
|
|
|
408
409
|
The bundled CNY rates were checked against the [official pricing page](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/) on 2026-09-10. Beijing weekday peak windows are 09:00–12:00 and 14:00–18:00; other times use half-price rates. Flash peak cache-miss-input/cache-hit-input/output rates are ¥2/¥0.04/¥8 per million tokens and Pro rates are ¥9/¥0.30/¥27; the current model name is `deepseek-flash`, and older Flash names keep those same rates. The provider has announced that from 2026-09-14T12:00+08:00 it serves `deepseek-v4-pro` from Flash and bills it at Flash rates, which the bundled entry records so that date does not overstate Pro usage. Separate cache writes use the uncached-input rate. An exact configured model price takes priority; otherwise `deepseek-official` names containing `pro` (case-insensitive) use Pro and all other names use Flash, including temporary model aliases. Other providers require explicit entries.
|
|
409
410
|
|
|
410
|
-
The default price validity starts at Beijing midnight on the verification date; this is a local estimate policy, not a claim about the official effective date. Earlier usage needs historical price entries. The recorded assistant settlement timestamp selects the rate; requests spanning a tariff boundary may differ from the invoice because the official page does not specify their attribution. Images use provider-reported tokens. A
|
|
411
|
+
The default price validity starts at Beijing midnight on the verification date; this is a local estimate policy, not a claim about the official effective date. Earlier usage needs historical price entries. The recorded assistant settlement timestamp selects the rate; requests spanning a tariff boundary may differ from the invoice because the official page does not specify their attribution. Images use provider-reported tokens. A scan folds the history again with the table it holds, so a stored total is a projection of the log and the table: editing `prices.json` re-prices the requests it covers on the next scan, and a request no entry covered is priced as soon as one does.
|
|
411
412
|
|
|
412
413
|
On first interactive launch, the client creates `~/.config/dsht/prices.json` (or `$XDG_CONFIG_HOME/dsht/prices.json`). `DSHT_CONFIG_DIR` overrides that directory. The JSON array contains price versions with `id`, `provider`, `model`, `currency: "CNY"`, `source`, inclusive `from`, optional exclusive `until`, `timezone`, weekday numbers (`0` Sunday), minute-of-day `windows`, and `peak`/`offPeak` rates named `input`, `cacheRead`, `cacheWrite`, `output`, per million tokens. To update prices, close the old interval with `until` and append a new version with a unique ID and matching `from`; overlapping intervals are rejected. Restart to load configuration changes. Price discovery is manual; the TUI does not scrape prices during startup.
|
|
413
414
|
|
|
414
|
-
Usage files live under `~/.local/state/dsht/cost/<origin-hash>/` (respecting `XDG_STATE_HOME`, or `DSHT_STATE_DIR` for the application state root).
|
|
415
|
+
Usage files live under `~/.local/state/dsht/cost/<origin-hash>/` (respecting `XDG_STATE_HOME`, or `DSHT_STATE_DIR` for the application state root). Each holds one session's folded totals: the session amount with its request and unpriced counts, the day bucket its last scan ran on, the decision-rules revision and a digest of the price table that produced them, and the reasons a total is inexact. They exclude prompts, tool bodies, credentials, cookies, and every per-request fact. The price file is configuration and these usage files are state, so only the former belongs in a settings backup. Writes use private temporary files and atomic replacement; each session keeps one fixed file whose recorded cut and rules revision are compared before writing, so an older scan cannot displace a newer one. The cache survives restart and does not need access to the host configuration directory. A file of another generation is ignored and rebuilt by the next scan.
|
|
415
416
|
|
|
416
417
|
## Client API
|
|
417
418
|
|
|
@@ -425,7 +426,7 @@ This repository publishes one public package, `@itookit/dsht`, from the `mushuan
|
|
|
425
426
|
|
|
426
427
|
| Field | Value |
|
|
427
428
|
| --- | --- |
|
|
428
|
-
| Name and version | `@itookit/dsht` `0.3.
|
|
429
|
+
| Name and version | `@itookit/dsht` `0.3.4` |
|
|
429
430
|
| Executable | `dsht`, or `npx @itookit/dsht` without installing |
|
|
430
431
|
| Library entries | `@itookit/dsht` and `@itookit/dsht/auth` |
|
|
431
432
|
| Author | lizlok@gmail.com |
|
|
@@ -463,7 +464,7 @@ node dist/cli/index.js --help
|
|
|
463
464
|
|
|
464
465
|
Tests use isolated HTTP/WebSocket hosts, drive the real Ink picker and composer, run the CLI in subprocesses, and project copied Harness v2 workspace-edit and v0 packed-chunk recordings. The repository needs no model credentials for these checks. The recording and expected transcript live under `tests/`; they do not depend on a parent checkout. Live model-provider behavior is not covered by these tests.
|
|
465
466
|
|
|
466
|
-
Source is organised by business domain under `src/`: `transport/` owns the host wire protocol and authentication, `session/` the transcript, history and interactions, `cost/` the
|
|
467
|
+
Source is organised by business domain under `src/`: `transport/` owns the host wire protocol and authentication, `session/` the transcript, history and interactions, `cost/` the folded billing ledger, `catalog/` models and presets, `controller/` the application facade, `ui/` everything React and Ink, `storage/` every filesystem operation, and `cli/` the composition root. Cross-domain imports go through each module's `index.ts`; `tests/architecture/dependencies.test.ts` rejects a forbidden direction.
|
|
467
468
|
|
|
468
469
|
`npm test` renders frames without styling, because the assertions and the recorded expectations in `tests/expected/` describe text. A test runner started from a terminal exports `FORCE_COLOR=1` to each test file, which makes Ink interleave SGR escapes between a prompt and its text; `npm run test:terminal` reproduces that environment on any host, and `prepublishOnly` runs it so a publish from a terminal validates what a terminal actually renders. Theme tests render separate truecolor and plain subprocesses with terminal and CI color detection isolated from the parent environment.
|
|
469
470
|
|
package/README.zh.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
`dsht` **本身不是 SSH 客户端**。它运行在普通终端中,因此可以直接工作在 SSH、嵌套 SSH、ProxyJump/跳板机、tmux 等远程终端环境里。只要你的终端能够到达运行 `dsht` 的主机,就可以继续控制同一套 DeepSeek Harness 会话。
|
|
38
38
|
|
|
39
|
-
除了远程控制,`dsht` 还内置了面向成本控制的用量统计:它按请求记录 token
|
|
39
|
+
除了远程控制,`dsht` 还内置了面向成本控制的用量统计:它按请求记录 token 用量,区分未缓存输入、缓存读取、缓存写入和输出,并结合模型、时间、高峰/空闲价格及版本化价格表,计算当前会话与今日的人民币费用估算。
|
|
40
40
|
|
|
41
41
|
主要特点:
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
- 排队消息、转向输入、轮次取消、审批和自由文本问题回答。
|
|
48
48
|
- 按服务端保存 cookie、自动重连和快照替换,方便断线后恢复控制。
|
|
49
49
|
- 面向脚本的 JSON/制表符工作区与会话列表,以及可复用的 HTTP 客户端。
|
|
50
|
-
-
|
|
50
|
+
- **成本感知**:会话与今日人民币费用估算,支持版本化高峰/空闲价格和 `/cost` 汇总。
|
|
51
51
|
|
|
52
52
|
## 为什么使用 dsht?
|
|
53
53
|
|
|
@@ -118,7 +118,7 @@ DeepSeek Harness 往往运行在性能更强、环境更完整的开发工作站
|
|
|
118
118
|
今日 + 前两个自然日
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
状态栏还可以持续显示本会话费用与括号内的今日合计(`¥: 1.23(5.00)`),便于在任务执行过程中及时发现成本变化,而不是等到账单出现后才知道消耗了多少。
|
|
122
122
|
|
|
123
123
|
这让 `dsht` 同时承担两个角色:
|
|
124
124
|
|
|
@@ -166,7 +166,7 @@ npm start
|
|
|
166
166
|
|
|
167
167
|
两种方式读取相同的 `DSH_URL` 和 `DSH_TOKEN` 变量。
|
|
168
168
|
|
|
169
|
-
使用 ↑/↓ 和 Enter 选择工作区,然后选择已有会话或 **New session**。**All sessions** 同时显示未归属注册工作区的会话。**Add workspace**
|
|
169
|
+
使用 ↑/↓ 和 Enter 选择工作区,然后选择已有会话或 **New session**。**All sessions** 同时显示未归属注册工作区的会话。**Add workspace (this directory)** 直接注册 `dsht` 自身所在的目录,只在服务端尚未注册它时出现;**Add workspace (host directory)** 接收服务端已有目录的绝对路径,该路径可能与本机文件系统不同,按 Esc 可以退回选择器。新建会话前必须选择工作区。
|
|
170
170
|
|
|
171
171
|
首次登录通过 `GET /` 兑换 token,并按 HTTP origin 保存 cookie。后续启动和列表命令自动复用 cookie,无需再次提供 token。默认目录为 `$XDG_STATE_HOME/dsht/auth`,未设置时使用 `~/.local/state/dsht/auth`;可通过 `--auth-dir` 或 `DSHT_AUTH_DIR` 覆盖。POSIX 下目录权限为 0700、cookie 文件为 0600;Windows 使用账户目录继承的访问控制。启动 token 永不保存。
|
|
172
172
|
|
|
@@ -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
|
| --- | --- |
|
|
@@ -319,7 +319,7 @@ Tab 补全开头的 slash 命令,多个候选时补到公共前缀。单行输
|
|
|
319
319
|
|
|
320
320
|
| 命令 | 操作 |
|
|
321
321
|
| --- | --- |
|
|
322
|
-
| `/ws` |
|
|
322
|
+
| `/ws` | 显示所有工作区与两条添加工作区入口,选中后打开其会话列表 |
|
|
323
323
|
| `/ws TARGET` | 按 ID、完整名称/路径或唯一 ID 前缀选择工作区 |
|
|
324
324
|
| `/resume` | 显示当前工作区的会话;未选择工作区时先引导选择 |
|
|
325
325
|
| `/resume TARGET` | 按 ID、完整标题或唯一 ID 前缀跨工作区打开会话 |
|
|
@@ -335,6 +335,7 @@ Tab 补全开头的 slash 命令,多个候选时补到公共前缀。单行输
|
|
|
335
335
|
| `/feedback TEXT` | 记录当前会话反馈 |
|
|
336
336
|
| `/export [local.zip]` | 下载会话日志 ZIP 到新的本地文件 |
|
|
337
337
|
| `/export-html [local.html]` | 将已加载会话保存为包含图表和公式的离线 HTML |
|
|
338
|
+
| `/coredump [tag]` | 在当前工作目录写出 V8 堆快照,用于内存诊断 |
|
|
338
339
|
| `/older` | 加载更早的历史 |
|
|
339
340
|
| `/history [text]` | 列出自己的提示词并可选筛选;Enter 跳到所选记录 |
|
|
340
341
|
| `/search <text>` | 逐页搜索历史,选择命中项后打开其位置 |
|
|
@@ -344,12 +345,12 @@ Tab 补全开头的 slash 命令,多个候选时补到公共前缀。单行输
|
|
|
344
345
|
| `/wsearch <text>` | 搜索服务端可见的所有工作区会话 |
|
|
345
346
|
| `/allow`, `/deny` | 回复当前审批;批准仅限一次 |
|
|
346
347
|
| `/status` | 展开或收起底部完整状态信息;`↑`/`↓` 滚动、`PgUp`/`PgDn` 翻屏 |
|
|
347
|
-
| `/cost` |
|
|
348
|
+
| `/cost` | 展开/收起会话与今日费用,并刷新用量 |
|
|
348
349
|
| `/think` | 显示思考及用户 prompt 摘要列表;↑/↓ 选择、Enter 跳转并展开 |
|
|
349
350
|
| `/think SEQ` | 切换一条已加载思考的展开状态;`live` 表示当前尝试 |
|
|
350
351
|
| `/help`, `/quit` | 列出每条命令及其说明,或退出 |
|
|
351
352
|
|
|
352
|
-
Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示匹配命令,`/help` 分页列出命令及其单行说明,PgUp/PgDn 翻页。`/help`、`/cost`、`/status` 面板保持打开,直到下一条命令或 Esc
|
|
353
|
+
Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示匹配命令,`/help` 分页列出命令及其单行说明,PgUp/PgDn 翻页。`/help`、`/cost`、`/status` 面板保持打开,直到下一条命令或 Esc;`/history` 还会在十秒后自动关闭,避免遗留列表一直占用输入框。Esc 保留输入内容。`/workspace`、`/workspaces` 是 `/ws` 的别名;`/session`、`/sessions` 是 `/resume` 的别名。名称可以包含空格,完整目标两侧的引号可选。不带引号的目标 `all` 保留给 `/resume all`;打开标题为 `all` 的会话时,使用 `/resume "all"` 或其 ID。目标有歧义时必须提供完整 ID。切换工作区会打开其会话列表并解除旧对话订阅;切换会话会同步工作区标签。两种操作均不会取消远程代理。
|
|
353
354
|
|
|
354
355
|
在输入末尾键入 `@`,可搜索所选会话**在服务端**工作目录中的文件和目录。使用 ↑/↓ 选择,Tab 或 Enter 插入;选择目录后继续补全其内部路径。带空格的路径使用 `@"path with spaces"`。Esc 关闭菜单,任务运行中时同时请求取消;关闭后 Enter 发送原样输入,包括未匹配到的路径。搜索失败时显示错误,不提交输入。补全针对输入末尾的引用,不跟踪已有文本内部的光标位置。
|
|
355
356
|
|
|
@@ -365,7 +366,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
365
366
|
|
|
366
367
|
审批提供 1. 允许一次、2. 拒绝、3. 停止当前轮次。输入框为空时,按 1–3 或 ↑/↓ 选择,再按 Enter 确认;初始不选中任何操作,Esc 清除高亮,请求重放时重新回到未选中。已有草稿时仍按正常文字输入处理,也保留 `/allow`、`/deny` 和 `/cancel` 命令。
|
|
367
368
|
|
|
368
|
-
用户问题显示题目进度、编号选项和说明。选择列表、待答问题、审批和文件补全与文本输入共用一个输入框边框。待答问题和审批会在输入框上方保留最近对话历史,历史视口使用剩余高度;对话框打开时减少上下留白,并在手动滚动或视口高度变化时刷新;可见选项数量按终端高度调整,并跟随当前高亮项滚动。输入框为空时,↑/↓ 或 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 响应中断可能导致投递状态不确定,手动重发前应检查会话记录。客户端不会自动重试修改请求。
|
|
369
370
|
|
|
370
371
|
标题固定在可滚动对话区域上方,输入框和状态栏保留在下方。底部不再常驻快捷键说明,完整快捷键放在 `/help`,选择器只显示当前需要的导航提示。顶部单行优先显示最新会话标题(无标题时回退到 ID),宽屏时在其后附上工作区名称,不再显示主机地址和连接状态。未选择会话时显示工作区名称或 All workspaces,下方为分隔线;`/status` 保留完整会话 ID。取消回执在后续历史消息到达时保持可见,直到服务端报告空闲;接受取消不表示工具进程已经退出。
|
|
371
372
|
|
|
@@ -379,13 +380,13 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
379
380
|
|
|
380
381
|
## 实时状态
|
|
381
382
|
|
|
382
|
-
底栏分组显示 `◐ Working · 8s · Ctrl+C Stop
|
|
383
|
+
底栏分组显示 `◐ Working · 8s · Ctrl+C Stop`、`● Ready`,或本客户端还欠一个审批/回答时的 `? Needs you`、模型与思考强度、本会话费用与「今天花费(历史总计)」、十格上下文进度条及百分比、会话轮次与累计 token 及缓存命中率。宽终端为运行状态预留固定宽度,完成后模型和指标保持对齐。窄终端依次回收留白、隐藏进度条、缩短模型名、省略次要指标,优先保留停止提示。`/status` 显示主机 URL、操作状态、工作区完整路径、完整供应商/模型、下次模型、各项用量、轮次、队列、后台任务和四位小数费用。`!` 表示有指标或模型目录错误,或计费覆盖不完整;详情中显示原因。运行中使用最近实际使用的模型,空闲时使用下次模型,新会话使用服务端模型目录默认值。服务端设置、凭据和适配器变更通知会刷新模型目录。
|
|
383
384
|
|
|
384
385
|
工作计时使用已加载日志的 `turn/start` 时间戳。缺少该时间戳时,详情面板中的 `(observed)` 表示从客户端观察到运行开始计时;重连可能重置此备用计时。服务端报告空闲后停止计时。运行状态涵盖模型生成、工具执行及审批等待,不仅是文本输出。断线时明确标注为最后已知状态。展开面板把相关值合并到一行,并用单行状态栏已有的紧凑计数(`Context ~40% (400.6K/1M) · 229.7M tok`),因此 24 行终端可以一屏看到全部详情;`↑`/`↓` 逐行滚动,`PgUp`/`PgDn` 翻屏,页脚标出可见区间与总数。
|
|
385
386
|
|
|
386
|
-
单行状态栏按价值而不是按列来保留分组:`◐ 6:18 · bash 1:08 · ^C │
|
|
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` 会接管状态标记(对话框暂停时钟时也一样),因为需要动手的正是这个欠下的回答。
|
|
387
388
|
|
|
388
|
-
轮次数来自完整会话的 `sessionStats.turns` 投影。上下文占用标为 `~`:Harness 将供应商用量与对话变化估算值、最新模型容量结合。Token 总量来自完整会话的 `tokenUsage` 投影,分别显示非缓存输入、输出、缓存读取和缓存写入;思考 token
|
|
389
|
+
轮次数来自完整会话的 `sessionStats.turns` 投影。上下文占用标为 `~`:Harness 将供应商用量与对话变化估算值、最新模型容量结合。Token 总量来自完整会话的 `tokenUsage` 投影,分别显示非缓存输入、输出、缓存读取和缓存写入;思考 token 已包含在输出中。缓存命中率是缓存读取占三个互斥提示侧桶(非缓存输入、缓存读取、缓存写入)之和的比例,遇到部分命中时增加小数位而不是报成 `100%`。本会话与当天费用来自账本按会话保存的切片,因此还没有切片的会话显示为未知,而不是当天的花费。总量随服务端用量投影更新,不按流式字符计数。缺失数据显示 `unknown` 或 `?`。重连时控制流基线整体替换状态,每个投影键的序号防止旧 follow 快照覆盖较新的指标。
|
|
389
390
|
|
|
390
391
|
默认使用 [Catppuccin Mocha](https://catppuccin.com/palette/) 主题:`❯ User` 为蓝色,`✦ Assistant` 为绿色,思考为淡紫色,工具为天蓝色,成功为绿色,错误为红色。紧凑状态栏中 Ready 为绿色、Working 为黄色、离线为红色、暂停原因与分隔符为柔和灰色,模型/强度为淡紫色、费用为天蓝色、用量为柔和灰色。上下文占用达到 80% 时从绿色变黄,95% 时变红;这只是视觉阈值,不代表服务端压缩触发条件。各组在 ANSI 着色之前按优先级装填,因此无色终端显示同样的文字。语义配色独立放在 `src/ui/theme/index.ts`,应用可单独接收主题,消息不保存 ANSI 样式。Ink 根据终端能力输出颜色,无色终端仍保留角色标记。工具调用显示名称和描述;命令与描述不同时,下一行以 `$` 显示命令第一行。没有描述时使用命令第一行、路径或查询作为摘要。各行按终端显示宽度截断;结果按调用 ID 在原条目上将 ⚙ 更新为 ✓ 或 ✗,不再重复新增结果条目,命令预览保留两格缩进。调用尚未加载时单独显示结果摘要,加载调用页后合并;嵌套结果正文保持隐藏。
|
|
391
392
|
|
|
@@ -393,7 +394,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
393
394
|
|
|
394
395
|
`/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` 查看,为会话标题留出空间。
|
|
395
396
|
|
|
396
|
-
历史分为语义消息块、prompt/思考摘要、独立折叠状态和行数索引。流式更新复用已提交历史的位置索引,只生成当前可见区域。每个会话的 LRU 最多保留 2,048 行已提交终端内容,移出缓存的行在回看时重建。已结束的旧版流式分片和不用展示的工具结果正文会释放,原始日志由服务端保存。服务端日志作为持久层,客户端作为可重载的内存层。实时历史默认以 2,000 条记录或 16 MiB 语义数据估算量为软限制(`--history-records`、`--history-mb`),触发回收后以限制的 75% 为目标,释放旧正文、摘要和排版缓存。切换会话会释放上一会话的 transcript。回看和思考导航期间保护已加载窗口;`/latest` 返回实时输出并恢复回收。离线历史、未结束流和最小近期尾部受保护,因此这些参数不是进程 RSS 硬上限。默认启用体积受限的运行时内存日志,位于 `<state>/memory.log`:每 30 秒一行 JSON,记录进程计数、保留记录数与字节数、是否处于回看保护状态、账本规模、布局行缓存、有界的数学与图表缓存,以及最近一次成本扫描重读的会话数、页数与事件数,用于区分真实增长与 V8 高水位。若运行时提供强制回收能力,样本还会记录回收后的堆;`npm run start:profile` 会以该能力加上堆快照信号启动,此时 `kill -USR2 <pid>`
|
|
397
|
+
历史分为语义消息块、prompt/思考摘要、独立折叠状态和行数索引。流式更新复用已提交历史的位置索引,只生成当前可见区域。每个会话的 LRU 最多保留 2,048 行已提交终端内容,移出缓存的行在回看时重建。已结束的旧版流式分片和不用展示的工具结果正文会释放,原始日志由服务端保存。服务端日志作为持久层,客户端作为可重载的内存层。实时历史默认以 2,000 条记录或 16 MiB 语义数据估算量为软限制(`--history-records`、`--history-mb`),触发回收后以限制的 75% 为目标,释放旧正文、摘要和排版缓存。切换会话会释放上一会话的 transcript。回看和思考导航期间保护已加载窗口;`/latest` 返回实时输出并恢复回收。离线历史、未结束流和最小近期尾部受保护,因此这些参数不是进程 RSS 硬上限。默认启用体积受限的运行时内存日志,位于 `<state>/memory.log`:每 30 秒一行 JSON,记录进程计数、保留记录数与字节数、是否处于回看保护状态、账本规模、布局行缓存、有界的数学与图表缓存,以及最近一次成本扫描重读的会话数、页数与事件数,用于区分真实增长与 V8 高水位。若运行时提供强制回收能力,样本还会记录回收后的堆;`npm run start:profile` 会以该能力加上堆快照信号启动,此时 `kill -USR2 <pid>` 即可写出快照。`/coredump [tag]` 无需信号或 profiling 运行时,即可把同样的 `.heapsnapshot` 产物写入客户端当前工作目录;`tag` 用于命名快照(默认 `snapshot`),V8 会同步序列化整个堆,因此客户端在文件写完前会暂停。快照请用 Chrome DevTools 打开分析,而不是终端。`--memory-log <路径>` 或 `DSHT_MEMORY_LOG` 可改路径,`--no-memory-log` 或 `DSHT_MEMORY_LOG=off` 可关闭。首次排版、改变终端宽度及展开特别大的单个内容块,仍需要处理对应全文。`npm run bench:history` 测量 500、2,000、10,000 条消息下的本地流式排版耗时,不含网络和模型时间。
|
|
397
398
|
|
|
398
399
|
## 费用估算
|
|
399
400
|
|
|
@@ -401,17 +402,17 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
401
402
|
|
|
402
403
|
> 这里的费用是基于 Harness 可见用量和本地价格配置得到的高精度估算,用于成本监控和控制;它不是供应商账户级账单,最终费用仍以供应商账单为准。
|
|
403
404
|
|
|
404
|
-
`/cost`
|
|
405
|
+
`/cost` 显示当前会话与今日的费用。日期使用 Asia/Shanghai。状态栏以两位小数显示本会话费用,括号内是今日合计(`¥: 1.23(113.00)`);这两个数不表示预算。`*` 表示该小计并不精确:请求缺少时间戳、没有价格覆盖,或扫描尚未覆盖全部会话;`/cost` 会说明原因。每个服务端 origin 使用独立账本;总额覆盖 HTTP 可见会话及之前缓存的会话,不是供应商账户级账单。
|
|
405
406
|
|
|
406
|
-
|
|
407
|
+
客户端每次连接后、每 60 秒、任务结束及打开 `/cost` 时在后台通过 HTTP 读取完整历史;每个新连接都会把全部会话完整重读一遍,因为客户端不在时服务端仍在工作,已记录的更新时间无法说明这段空档里发生了什么;同一次连接内,服务端更新时间未变的空闲会话跳过扫描。计费不会发起模型请求。显式刷新时可按 Esc 或 Ctrl+C 取消。账本分别统计未缓存输入、缓存读/写和输出,思考 token 已包含在输出中。重试单独计费,同一次尝试的替换用量更新原记录,fork 继承历史不重复计费。没有可用用量、没有结算时间戳,或带官方价目表并不定价的缓存写入桶的请求只计入未计价而不猜测;没有条目覆盖的模型保持未计价,未列出的供应商不会套用官方价目。每个会话独立读取:某个会话不可达或被拒绝时只计为失败数量并继续扫描,不会中止整轮;子代理会话按其持久父级地址读取。扫描失败保留并标明部分缓存结果。
|
|
407
408
|
|
|
408
409
|
内置人民币价格于 2026-09-10 根据[官方价格页](https://api-docs.deepseek.com/zh-cn/quick_start/pricing/)核对。北京时间工作日 09:00–12:00、14:00–18:00 为高峰,其余时段半价。Flash 高峰未命中输入/缓存命中输入/输出为每百万 token ¥2/¥0.04/¥8,Pro 为 ¥9/¥0.30/¥27;当前模型名为 `deepseek-flash`,旧 Flash 名称沿用同一费率。供应方已公告自北京时间 2026-09-14 12:00 起将 `deepseek-v4-pro` 交由 Flash 服务并按 Flash 价格计费,内置条目已记录该变更,避免此后高估 Pro 用量。单列的缓存写入按未命中输入价计算。配置中的精确模型价格优先;否则 `deepseek-official` 模型名包含 `pro`(不区分大小写)时按 Pro 计价,其余名称包括临时别名均按 Flash 计价。其他供应商需要显式配置。
|
|
409
410
|
|
|
410
|
-
默认价格有效期从核对日期的北京时间零点开始,这是本地估算规则,不代表官方价格生效日期。更早用量需要补充历史价格版本。程序按助手请求结算记录的时间选择单价;官方未说明跨时段请求的归属,因此边界附近的估算可能与账单不同。图片使用供应商报告的 token
|
|
411
|
+
默认价格有效期从核对日期的北京时间零点开始,这是本地估算规则,不代表官方价格生效日期。更早用量需要补充历史价格版本。程序按助手请求结算记录的时间选择单价;官方未说明跨时段请求的归属,因此边界附近的估算可能与账单不同。图片使用供应商报告的 token 数。每次扫描都按当前加载的价格表重新折叠历史,因此落盘的总额是日志与价格表的投影:修改 `prices.json` 会在下一次扫描时重新计价它覆盖的请求,此前没有条目覆盖的请求则在出现覆盖后立即计价。
|
|
411
412
|
|
|
412
413
|
首次交互启动会创建 `~/.config/dsht/prices.json`(或 `$XDG_CONFIG_HOME/dsht/prices.json`),可用 `DSHT_CONFIG_DIR` 覆盖目录。JSON 数组中的价格版本包含 `id`、`provider`、`model`、`currency: "CNY"`、`source`、包含起点的 `from`、可选且不含终点的 `until`、`timezone`、星期数字 `weekdays`(`0` 为周日)、日内分钟区间 `windows`,以及 `peak`/`offPeak` 下每百万 token 的 `input`、`cacheRead`、`cacheWrite`、`output` 单价。调价时用 `until` 结束旧区间,再添加唯一 ID 且 `from` 衔接的新版本;程序拒绝重叠区间。重启后读取配置修改;价格由用户维护,启动时不抓取网页价格。
|
|
413
414
|
|
|
414
|
-
用量文件位于 `~/.local/state/dsht/cost/<origin-hash>/`,遵循 `XDG_STATE_HOME`,也可通过 `DSHT_STATE_DIR`
|
|
415
|
+
用量文件位于 `~/.local/state/dsht/cost/<origin-hash>/`,遵循 `XDG_STATE_HOME`,也可通过 `DSHT_STATE_DIR` 指定应用状态根目录。每个文件保存一个会话折叠后的总额:会话金额及其请求数与未计价数、最近一次扫描所在自然日的当天分桶、决策规则版本与该次折叠所用价格表的摘要,以及总额不精确的原因。文件不含提示词、工具正文、凭据、cookie,也不含任何逐请求信息。价格文件属于配置,这些用量文件属于状态,因此只有前者需要纳入设置备份。写入使用私有临时文件及原子替换;每个会话一个固定文件,写入前比较文件中记录的 cut 与规则版本,因此旧扫描无法覆盖较新的一次。缓存跨重启保留,不需要访问服务端配置目录。其他代数的文件会被忽略,并由下一次扫描重建。
|
|
415
416
|
|
|
416
417
|
## 客户端接口
|
|
417
418
|
|
|
@@ -425,7 +426,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
425
426
|
|
|
426
427
|
| 字段 | 值 |
|
|
427
428
|
| --- | --- |
|
|
428
|
-
| 名称与版本 | `@itookit/dsht` `0.3.
|
|
429
|
+
| 名称与版本 | `@itookit/dsht` `0.3.4` |
|
|
429
430
|
| 可执行命令 | `dsht`,不安装时用 `npx @itookit/dsht` |
|
|
430
431
|
| 库入口 | `@itookit/dsht` 和 `@itookit/dsht/auth` |
|
|
431
432
|
| 作者 | lizlok\@gmail.com |
|
|
@@ -463,7 +464,7 @@ node dist/cli/index.js --help
|
|
|
463
464
|
|
|
464
465
|
测试使用隔离的 HTTP/WebSocket 服务,驱动实际 Ink 选择器和输入框,在子进程中运行 CLI,并投影复制的 Harness v2 工作区编辑记录和 v0 压缩 chunk 记录。这些检查不需要模型凭据。记录和预期对话输出位于 `tests/`,不依赖父仓库。测试不覆盖真实模型供应商行为。
|
|
465
466
|
|
|
466
|
-
源码在 `src/` 下按业务域组织:`transport/` 负责服务端 wire 协议与认证,`session/` 负责对话、历史与交互,`cost/`
|
|
467
|
+
源码在 `src/` 下按业务域组织:`transport/` 负责服务端 wire 协议与认证,`session/` 负责对话、历史与交互,`cost/` 负责折叠计费账本,`catalog/` 负责模型与 preset,`controller/` 是应用门面,`ui/` 承载全部 React 与 Ink,`storage/` 负责全部文件系统操作,`cli/` 是组装入口。跨模块导入统一走各模块的 `index.ts`;`tests/architecture/dependencies.test.ts` 会拒绝禁止的依赖方向。
|
|
467
468
|
|
|
468
469
|
`npm test` 渲染不带样式的帧,因为断言和 `tests/expected/` 中的预期输出描述的是文本。从终端启动的测试运行器会向每个测试文件导出 `FORCE_COLOR=1`,使 Ink 在提示符与文本之间插入 SGR 转义序列;`npm run test:terminal` 在任何主机上复现该环境,`prepublishOnly` 也会运行它,因此从终端发布时验证的就是终端实际渲染的结果。 主题测试在独立子进程中分别渲染真彩色和纯文本,并隔离父进程中影响终端和 CI 颜色检测的环境设置。
|
|
469
470
|
|
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; });
|