@itookit/dsht 0.3.4 → 0.3.8

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.
Files changed (42) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -3
  3. package/README.zh.md +4 -3
  4. package/dist/cli/dsht.js +5 -2
  5. package/dist/controller/controller.d.ts +113 -1
  6. package/dist/controller/controller.js +145 -2
  7. package/dist/cost/controller.d.ts +5 -0
  8. package/dist/cost/controller.js +2 -1
  9. package/dist/cost/scanner.d.ts +4 -2
  10. package/dist/cost/scanner.js +6 -3
  11. package/dist/session/controller.d.ts +150 -1
  12. package/dist/session/controller.js +388 -29
  13. package/dist/session/history.d.ts +21 -1
  14. package/dist/session/history.js +15 -0
  15. package/dist/session/index.d.ts +2 -0
  16. package/dist/session/index.js +1 -0
  17. package/dist/session/info.d.ts +262 -0
  18. package/dist/session/info.js +326 -0
  19. package/dist/session/transcript.d.ts +37 -1
  20. package/dist/session/transcript.js +73 -0
  21. package/dist/shell/controller.d.ts +67 -0
  22. package/dist/shell/controller.js +126 -0
  23. package/dist/shell/index.d.ts +5 -0
  24. package/dist/shell/index.js +3 -0
  25. package/dist/shell/runner.d.ts +28 -0
  26. package/dist/shell/runner.js +108 -0
  27. package/dist/state.d.ts +3 -2
  28. package/dist/state.js +2 -2
  29. package/dist/ui/app.js +206 -177
  30. package/dist/ui/chat/history-view.js +1 -1
  31. package/dist/ui/chat/shell-view.d.ts +34 -0
  32. package/dist/ui/chat/shell-view.js +111 -0
  33. package/dist/ui/chat/status.js +4 -4
  34. package/dist/ui/commands/parse.d.ts +5 -0
  35. package/dist/ui/commands/parse.js +9 -0
  36. package/dist/ui/dialogs/index.d.ts +3 -6
  37. package/dist/ui/theme/index.d.ts +5 -0
  38. package/dist/ui/theme/index.js +2 -1
  39. package/dsht-m.png +0 -0
  40. package/package.json +1 -1
  41. package/dist/ui/input/history.d.ts +0 -19
  42. package/dist/ui/input/history.js +0 -43
@@ -12,6 +12,6 @@ import { useTheme } from "../theme/index.js";
12
12
  */
13
13
  export function HistoryViewport({ rows }) {
14
14
  const theme = useTheme();
15
- return _jsx(Box, { flexDirection: "column", children: rows.map((row, index) => _jsx(Text, { color: theme.colors[row.kind], bold: row.bold, children: row.spans?.length ? row.spans.map((span, position) => _jsx(Text, { bold: span.bold, italic: span.italic, underline: span.underline, strikethrough: span.strikethrough, inverse: span.inverse, children: span.text }, position))
15
+ return _jsx(Box, { flexDirection: "column", children: rows.map((row, index) => _jsx(Text, { bold: row.bold, color: row.highlight ? theme.shell.foreground : theme.colors[row.kind], backgroundColor: row.highlight ? theme.shell.background : undefined, children: row.spans?.length ? row.spans.map((span, position) => _jsx(Text, { bold: span.bold, italic: span.italic, underline: span.underline, strikethrough: span.strikethrough, inverse: span.inverse, children: span.text }, position))
16
16
  : row.text === '' ? ' ' : row.text }, index)) });
17
17
  }
@@ -0,0 +1,34 @@
1
+ import { type HistoryRow } from '../../session/history.ts';
2
+ import type { ShellBlock } from '../../shell/index.ts';
3
+ /** The part of a transcript layout this module needs to place a block. */
4
+ export interface RowSource {
5
+ /** Number of host rows, live tail included. */
6
+ length: number;
7
+ /** Host rows in a range. */
8
+ viewport(start: number, end: number): HistoryRow[];
9
+ /** Projected messages in ascending sequence order. */
10
+ messages: readonly {
11
+ seq: number;
12
+ }[];
13
+ /** Row index where each visible message starts. */
14
+ offsets: ReadonlyMap<number, number>;
15
+ }
16
+ /** Rows for one run: the command bar, then its wrapped and indented output.
17
+ * @param run - Block from the shell controller.
18
+ * @param width - Available terminal columns.
19
+ * @returns Rows in display order.
20
+ */
21
+ export declare function blockRows(run: ShellBlock, width: number): HistoryRow[];
22
+ /** Splice every retained block into a transcript layout.
23
+ *
24
+ * Equal anchors keep creation order, and an anchor older than a block already placed is clamped to
25
+ * it, so the merged stream stays ordered even as history is paged in behind the reader.
26
+ * @param layout - Transcript layout to merge with.
27
+ * @param runs - Blocks from the shell controller, oldest first.
28
+ * @param width - Available terminal columns.
29
+ * @returns The merged row count and a reader over a merged range.
30
+ */
31
+ export declare function mergeShellRuns(layout: RowSource, runs: readonly ShellBlock[], width: number): {
32
+ total: number;
33
+ viewport(start: number, end: number): HistoryRow[];
34
+ };
@@ -0,0 +1,111 @@
1
+ /** Local `!` blocks as transcript rows: a highlighted command bar, then its indented output.
2
+ *
3
+ * A block is spliced into the conversation where it happened — after the newest record the session
4
+ * had when the command started — so it scrolls away like any message instead of sitting pinned to
5
+ * the bottom of the screen.
6
+ */
7
+ import stringWidth from 'string-width';
8
+ import { plainRows } from "../../session/history.js";
9
+ /** First output row's marker, so a block reads as the command's result. */
10
+ const MARKER = ' ⎿ ';
11
+ /** Rows after the first, aligned under the marker. */
12
+ const GUTTER = ' ';
13
+ /** Rows for one run: the command bar, then its wrapped and indented output.
14
+ * @param run - Block from the shell controller.
15
+ * @param width - Available terminal columns.
16
+ * @returns Rows in display order.
17
+ */
18
+ export function blockRows(run, width) {
19
+ const reserve = Math.max(stringWidth(MARKER), stringWidth(GUTTER));
20
+ const rows = [{ text: `! ${run.command}`, kind: 'shell', bold: true, highlight: true }];
21
+ const body = [
22
+ ...(run.dropped > 0 ? [`… ${run.dropped} earlier lines dropped …`] : []),
23
+ ...run.lines,
24
+ ];
25
+ if (!body.length)
26
+ body.push(run.status === 'running' ? 'running…' : '(no output)');
27
+ body.forEach((line, index) => {
28
+ const wrapped = plainRows(line, Math.max(8, width - reserve), 'shell');
29
+ wrapped.forEach((row, position) => {
30
+ rows.push({ ...row, text: `${index === 0 && position === 0 ? MARKER : GUTTER}${row.text}` });
31
+ });
32
+ });
33
+ if (run.status === 'exited' && (run.signal != null || (run.code ?? 0) !== 0)) {
34
+ rows.push({ text: `${GUTTER}exit ${run.signal ?? run.code}`, kind: 'muted' });
35
+ }
36
+ return rows;
37
+ }
38
+ /** Host row index where a block anchored at `anchor` belongs.
39
+ *
40
+ * It goes after every row of the newest message it followed, which is the start of the next message,
41
+ * or the end of the layout when nothing newer is loaded.
42
+ * @param layout - Transcript layout to place into.
43
+ * @param anchor - Durable sequence the command followed.
44
+ * @returns Host row index.
45
+ */
46
+ function rowAfter(layout, anchor) {
47
+ let low = 0, high = layout.messages.length;
48
+ while (low < high) {
49
+ const mid = (low + high) >> 1;
50
+ if (layout.messages[mid].seq <= anchor)
51
+ low = mid + 1;
52
+ else
53
+ high = mid;
54
+ }
55
+ if (low >= layout.messages.length)
56
+ return layout.length;
57
+ return layout.offsets.get(layout.messages[low].seq) ?? layout.length;
58
+ }
59
+ /** Splice every retained block into a transcript layout.
60
+ *
61
+ * Equal anchors keep creation order, and an anchor older than a block already placed is clamped to
62
+ * it, so the merged stream stays ordered even as history is paged in behind the reader.
63
+ * @param layout - Transcript layout to merge with.
64
+ * @param runs - Blocks from the shell controller, oldest first.
65
+ * @param width - Available terminal columns.
66
+ * @returns The merged row count and a reader over a merged range.
67
+ */
68
+ export function mergeShellRuns(layout, runs, width) {
69
+ const placements = [];
70
+ let cursor = 0;
71
+ for (const run of runs) {
72
+ const at = Math.max(cursor, Math.min(layout.length, rowAfter(layout, run.anchor)));
73
+ placements.push({ at, rows: blockRows(run, width) });
74
+ cursor = at;
75
+ }
76
+ // Segments alternate host rows and block rows in merged index order.
77
+ const segments = [];
78
+ let merged = 0, host = 0;
79
+ for (const placement of placements) {
80
+ if (placement.at > host) {
81
+ segments.push({ from: merged, count: placement.at - host, host });
82
+ merged += placement.at - host;
83
+ host = placement.at;
84
+ }
85
+ segments.push({ from: merged, count: placement.rows.length, rows: placement.rows });
86
+ merged += placement.rows.length;
87
+ }
88
+ if (host < layout.length)
89
+ segments.push({ from: merged, count: layout.length - host, host });
90
+ const total = merged + Math.max(0, layout.length - host);
91
+ return {
92
+ total,
93
+ viewport(start, end) {
94
+ const rows = [];
95
+ for (const segment of segments) {
96
+ const segmentStart = segment.from, segmentEnd = segment.from + segment.count;
97
+ if (segmentEnd <= start || segmentStart >= end)
98
+ continue;
99
+ const from = Math.max(0, start - segmentStart);
100
+ const to = Math.min(segment.count, end - segmentStart);
101
+ if (to <= from)
102
+ continue;
103
+ if (segment.rows)
104
+ rows.push(...segment.rows.slice(from, to));
105
+ else
106
+ rows.push(...layout.viewport(segment.host + from, segment.host + to));
107
+ }
108
+ return rows;
109
+ },
110
+ };
111
+ }
@@ -236,7 +236,7 @@ export const StatusBar = memo(function StatusBar({ controller, expanded = false,
236
236
  const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
237
237
  const view = controller.telemetry.view(state.sessionId);
238
238
  const costs = controller.costs;
239
- const sessionCost = costs?.hasSession(state.sessionId) ? costText(costs.total(state.sessionId)) : '?';
239
+ const sessionCost = controller.sessionCostText;
240
240
  const todayCost = costs ? costText(costs.today()) : '?';
241
241
  // `*` belongs to costText alone; incomplete coverage is a separate degradation, reported by `!`.
242
242
  const coverage = costs?.coverage ?? 'complete';
@@ -258,7 +258,7 @@ export const StatusBar = memo(function StatusBar({ controller, expanded = false,
258
258
  const billed = buckets[0] === undefined || buckets[2] === undefined ? undefined : buckets[0] + buckets[2] + (buckets[3] ?? 0);
259
259
  const hit = cacheHitText(buckets[2], billed);
260
260
  const compactCount = (value) => value === undefined ? '?' : compactNumber.format(value);
261
- const phase = state.transcript.livePhase;
261
+ const phase = state.session.record.livePhase;
262
262
  const clock = running && since !== undefined ? ` ${clockText(now - since)}` : '';
263
263
  const phaseLabel = phase === undefined ? undefined
264
264
  : phase.kind === 'tool' ? `${phase.name ?? 'tool'} ${phaseText(now - phase.startedAt)}`
@@ -325,7 +325,7 @@ const StatusDetails = memo(function StatusDetails({ controller, theme, width, no
325
325
  const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
326
326
  const view = controller.telemetry.view(state.sessionId);
327
327
  const costs = controller.costs;
328
- const sessionCost = costs?.hasSession(state.sessionId) ? costText(costs.total(state.sessionId)) : '?';
328
+ const sessionCost = controller.sessionCostText;
329
329
  const todayCost = costs ? costText(costs.today()) : '?';
330
330
  // `*` belongs to costText alone; incomplete coverage is a separate degradation, reported by `!`.
331
331
  const coverage = costs?.coverage ?? 'complete';
@@ -340,7 +340,7 @@ const StatusDetails = memo(function StatusDetails({ controller, theme, width, no
340
340
  { key: 'activity', color: state.pending.length > 0 ? theme.status.critical : running ? theme.colors.context : theme.colors.muted, text: state.pending.length > 0
341
341
  ? '? Needs you · answer the request above to continue'
342
342
  : running
343
- ? `◐ Working · ${duration}${state.transcript.activeTurnStartedAt === undefined ? ' (observed)' : ''} · Ctrl+C Stop`
343
+ ? `◐ Working · ${duration}${state.session.record.activeTurnStartedAt === undefined ? ' (observed)' : ''} · Ctrl+C Stop`
344
344
  : '● Ready · Ctrl+C exit' },
345
345
  { key: 'host', text: `${safeText(controller.base)} · ${safeText(state.status)}${!state.online ? ' · offline, last known status' : ''}` },
346
346
  ...state.sessionId
@@ -56,6 +56,11 @@ export type Submission = {
56
56
  } | {
57
57
  kind: 'hostCommand';
58
58
  line: string;
59
+ }
60
+ /** A local `!` command, run on this machine rather than the host. */
61
+ | {
62
+ kind: 'shell';
63
+ command: string;
59
64
  } | {
60
65
  kind: 'export';
61
66
  destination?: string;
@@ -17,6 +17,15 @@ export function classifySubmission(raw, context) {
17
17
  const value = raw.trim();
18
18
  if (!value)
19
19
  return { kind: 'ignore' };
20
+ // `!` runs on the machine this client is on; it never reaches the host or the model.
21
+ if (value.startsWith('!')) {
22
+ const command = value.slice(1).trim();
23
+ if (!command)
24
+ return { kind: 'error', message: 'Type a command after !' };
25
+ if (context.screen !== 'chat')
26
+ return { kind: 'error', message: 'Select a session first' };
27
+ return { kind: 'shell', command };
28
+ }
20
29
  if (value === '/copy')
21
30
  return { kind: 'copy' };
22
31
  if (value === '/quit')
@@ -27,12 +27,9 @@ export declare function RemovalDialog({ removal, enabled, canSelect, onCancel, o
27
27
  onCancel(): void;
28
28
  onConfirm(): void;
29
29
  }): import("react").JSX.Element;
30
- /** Two-step model and reasoning-effort selector. */
31
- export interface ModelState {
32
- catalog: ObjectValue;
33
- provider?: string;
34
- model?: ObjectValue;
35
- }
30
+ import type { ModelState } from '../../session/index.ts';
31
+ /** Two-step model and reasoning-effort selector; the state shape is shared with the session. */
32
+ export type { ModelState };
36
33
  /** Model routes and adapter-owned reasoning efforts.
37
34
  * @param props - Catalog, current step and its selection actions.
38
35
  * @returns The model or effort picker.
@@ -3,6 +3,11 @@ import type { RowKind } from '../../session/history.ts';
3
3
  export interface Theme {
4
4
  name: string;
5
5
  colors: Record<RowKind, string>;
6
+ /** Bar drawn behind a local `!` command line, so it reads as this machine rather than the agent. */
7
+ shell: {
8
+ background: string;
9
+ foreground: string;
10
+ };
6
11
  status: Record<'working' | 'ready' | 'offline' | 'model' | 'cost' | 'context' | 'warning' | 'critical' | 'usage', string>;
7
12
  accent: string;
8
13
  border: string;
@@ -4,7 +4,8 @@ import { createContext, useContext } from 'react';
4
4
  export const mocha = {
5
5
  name: 'Catppuccin Mocha',
6
6
  colors: { user: '#89b4fa', assistant: '#a6e3a1', context: '#f9e2af', text: '#cdd6f4',
7
- reasoning: '#cba6f7', tool: '#89dceb', success: '#a6e3a1', error: '#f38ba8', muted: '#a6adc8' },
7
+ reasoning: '#cba6f7', tool: '#89dceb', success: '#a6e3a1', error: '#f38ba8', muted: '#a6adc8', shell: '#a6adc8' },
8
+ shell: { background: '#cdd6f4', foreground: '#1e1e2e' },
8
9
  status: { working: '#f9e2af', ready: '#a6e3a1', offline: '#f38ba8', model: '#cba6f7',
9
10
  cost: '#89dceb', context: '#a6e3a1', warning: '#f9e2af', critical: '#f38ba8', usage: '#a6adc8' },
10
11
  accent: '#cba6f7', border: '#6c7086',
package/dsht-m.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itookit/dsht",
3
- "version": "0.3.4",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "Remote-first TUI for DeepSeek Harness with SSH-friendly mobile access and request-level cost tracking",
6
6
  "engines": {
@@ -1,19 +0,0 @@
1
- /** Bounded, process-local recall history; never stores transcripts or writes prompt text to disk. */
2
- export declare class InputHistory {
3
- private entries;
4
- private bytes;
5
- private position;
6
- private draft;
7
- /** Remember submitted input, coalescing consecutive duplicates and enforcing both budgets.
8
- * @param value - Submitted command or prompt, excluding interaction answers.
9
- */
10
- record(value: string): void;
11
- /** Leave recall navigation when the composer is edited or otherwise replaced. */
12
- reset(): void;
13
- /** Recall older/newer input, restoring the original unsent draft at the end.
14
- * @param direction - Negative for older input, positive for newer input.
15
- * @param current - Current composer content before beginning recall.
16
- * @returns Recalled input, or the original draft when returning to the newest position.
17
- */
18
- move(direction: -1 | 1, current: string): string;
19
- }
@@ -1,43 +0,0 @@
1
- /** Bounded, process-local recall history; never stores transcripts or writes prompt text to disk. */
2
- export class InputHistory {
3
- entries = [];
4
- bytes = 0;
5
- position;
6
- draft = '';
7
- /** Remember submitted input, coalescing consecutive duplicates and enforcing both budgets.
8
- * @param value - Submitted command or prompt, excluding interaction answers.
9
- */
10
- record(value) {
11
- this.reset();
12
- if (!value || this.entries.at(-1) === value || value.length * 2 > 256 * 1024)
13
- return;
14
- this.entries.push(value);
15
- this.bytes += value.length * 2;
16
- while (this.entries.length > 200 || this.bytes > 256 * 1024)
17
- this.bytes -= this.entries.shift().length * 2;
18
- }
19
- /** Leave recall navigation when the composer is edited or otherwise replaced. */
20
- reset() { this.position = undefined; this.draft = ''; }
21
- /** Recall older/newer input, restoring the original unsent draft at the end.
22
- * @param direction - Negative for older input, positive for newer input.
23
- * @param current - Current composer content before beginning recall.
24
- * @returns Recalled input, or the original draft when returning to the newest position.
25
- */
26
- move(direction, current) {
27
- if (!this.entries.length)
28
- return current;
29
- if (this.position === undefined) {
30
- if (direction > 0)
31
- return current;
32
- this.position = this.entries.length;
33
- this.draft = current;
34
- }
35
- this.position = Math.max(0, Math.min(this.entries.length, this.position + direction));
36
- if (this.position === this.entries.length) {
37
- const draft = this.draft;
38
- this.reset();
39
- return draft;
40
- }
41
- return this.entries[this.position];
42
- }
43
- }