@itookit/dsht 0.3.7 → 0.5.1

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 (132) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +31 -11
  3. package/README.zh.md +31 -11
  4. package/dist/cli/dsht.js +207 -19
  5. package/dist/cli/startup.d.ts +40 -0
  6. package/dist/cli/startup.js +295 -0
  7. package/dist/cli/trace-summary.d.ts +78 -0
  8. package/dist/cli/trace-summary.js +241 -0
  9. package/dist/cli/verifier.d.ts +60 -0
  10. package/dist/cli/verifier.js +242 -0
  11. package/dist/contracts.d.ts +344 -0
  12. package/dist/contracts.js +1 -0
  13. package/dist/controller/commands.d.ts +47 -0
  14. package/dist/controller/commands.js +322 -0
  15. package/dist/controller/connection.d.ts +11 -29
  16. package/dist/controller/connection.js +26 -60
  17. package/dist/controller/controller.d.ts +619 -164
  18. package/dist/controller/controller.js +1420 -141
  19. package/dist/controller/index.d.ts +8 -1
  20. package/dist/controller/index.js +5 -0
  21. package/dist/controller/loop-contract.d.ts +136 -0
  22. package/dist/controller/loop-contract.js +308 -0
  23. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  24. package/dist/controller/loop-prompts-schema.js +144 -0
  25. package/dist/controller/loop-prompts.d.ts +55 -0
  26. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  27. package/dist/controller/loop-prompts.generated.js +185 -0
  28. package/dist/controller/loop-prompts.js +104 -0
  29. package/dist/controller/loop-protocols.d.ts +39 -0
  30. package/dist/controller/loop-protocols.js +115 -0
  31. package/dist/controller/loop.d.ts +275 -0
  32. package/dist/controller/loop.js +378 -0
  33. package/dist/controller/prompts.d.ts +54 -0
  34. package/dist/controller/prompts.js +162 -0
  35. package/dist/controller/trace-log.d.ts +45 -0
  36. package/dist/controller/trace-log.js +144 -0
  37. package/dist/controller/verifier.d.ts +126 -0
  38. package/dist/controller/verifier.js +75 -0
  39. package/dist/cost/index.d.ts +1 -1
  40. package/dist/cost/index.js +1 -1
  41. package/dist/cost/ledger.d.ts +0 -1
  42. package/dist/cost/ledger.js +0 -1
  43. package/dist/json.d.ts +18 -0
  44. package/dist/json.js +19 -0
  45. package/dist/references.d.ts +25 -0
  46. package/dist/references.js +26 -0
  47. package/dist/session/connection-view.d.ts +2 -11
  48. package/dist/session/controller.d.ts +82 -72
  49. package/dist/session/controller.js +211 -209
  50. package/dist/session/history.d.ts +9 -1
  51. package/dist/session/history.js +1 -9
  52. package/dist/session/index.d.ts +9 -4
  53. package/dist/session/index.js +7 -3
  54. package/dist/session/info.d.ts +25 -52
  55. package/dist/session/info.js +39 -25
  56. package/dist/session/markdown.js +1 -1
  57. package/dist/session/math.js +1 -1
  58. package/dist/session/mutation-gate.d.ts +51 -0
  59. package/dist/session/mutation-gate.js +73 -0
  60. package/dist/session/navigation.d.ts +2 -89
  61. package/dist/session/navigation.js +2 -129
  62. package/dist/session/peek.d.ts +38 -0
  63. package/dist/session/peek.js +103 -0
  64. package/dist/session/references.d.ts +2 -20
  65. package/dist/session/references.js +1 -26
  66. package/dist/session/runtime.d.ts +26 -0
  67. package/dist/session/runtime.js +28 -0
  68. package/dist/session/telemetry.d.ts +12 -13
  69. package/dist/session/telemetry.js +27 -58
  70. package/dist/session/transcript.d.ts +0 -6
  71. package/dist/session/transcript.js +2 -15
  72. package/dist/session/types.d.ts +25 -0
  73. package/dist/session/types.js +0 -1
  74. package/dist/session-title.d.ts +9 -0
  75. package/dist/session-title.js +21 -0
  76. package/dist/shell/controller.d.ts +97 -0
  77. package/dist/shell/controller.js +158 -0
  78. package/dist/shell/index.d.ts +5 -0
  79. package/dist/shell/index.js +3 -0
  80. package/dist/shell/runner.d.ts +38 -0
  81. package/dist/shell/runner.js +147 -0
  82. package/dist/slash/index.d.ts +10 -0
  83. package/dist/slash/index.js +7 -0
  84. package/dist/slash/parse.d.ts +166 -0
  85. package/dist/slash/parse.js +259 -0
  86. package/dist/slash/pipeline.d.ts +140 -0
  87. package/dist/slash/pipeline.js +115 -0
  88. package/dist/slash/registry.d.ts +88 -0
  89. package/dist/slash/registry.js +177 -0
  90. package/dist/state.d.ts +14 -4
  91. package/dist/state.js +3 -2
  92. package/dist/text.d.ts +28 -0
  93. package/dist/text.js +55 -0
  94. package/dist/transport/events.d.ts +104 -0
  95. package/dist/transport/events.js +149 -0
  96. package/dist/transport/wire.d.ts +9 -17
  97. package/dist/transport/wire.js +2 -27
  98. package/dist/ui/app.js +865 -431
  99. package/dist/ui/chat/header.js +1 -1
  100. package/dist/ui/chat/history-view.d.ts +1 -1
  101. package/dist/ui/chat/history-view.js +1 -1
  102. package/dist/ui/chat/loop-status.d.ts +11 -0
  103. package/dist/ui/chat/loop-status.js +28 -0
  104. package/dist/ui/chat/navigation-model.d.ts +86 -0
  105. package/dist/ui/chat/navigation-model.js +107 -0
  106. package/dist/ui/chat/shell-view.d.ts +47 -0
  107. package/dist/ui/chat/shell-view.js +145 -0
  108. package/dist/ui/chat/status.d.ts +47 -3
  109. package/dist/ui/chat/status.js +65 -50
  110. package/dist/ui/chat/viewport.d.ts +1 -1
  111. package/dist/ui/dialogs/cost.d.ts +21 -4
  112. package/dist/ui/dialogs/cost.js +7 -12
  113. package/dist/ui/dialogs/index.d.ts +22 -5
  114. package/dist/ui/dialogs/index.js +19 -3
  115. package/dist/ui/dialogs/loop.d.ts +43 -0
  116. package/dist/ui/dialogs/loop.js +224 -0
  117. package/dist/ui/dialogs/peek.d.ts +25 -0
  118. package/dist/ui/dialogs/peek.js +35 -0
  119. package/dist/ui/dialogs/picker.d.ts +2 -0
  120. package/dist/ui/dialogs/picker.js +4 -2
  121. package/dist/ui/input/mouse.d.ts +12 -2
  122. package/dist/ui/input/mouse.js +20 -7
  123. package/dist/ui/input/references.d.ts +1 -1
  124. package/dist/ui/status/model.d.ts +7 -0
  125. package/dist/ui/status/model.js +5 -0
  126. package/dist/ui/theme/index.d.ts +6 -1
  127. package/dist/ui/theme/index.js +2 -1
  128. package/package.json +6 -4
  129. package/dist/ui/commands/parse.d.ts +0 -99
  130. package/dist/ui/commands/parse.js +0 -126
  131. package/dist/ui/commands/registry.d.ts +0 -33
  132. package/dist/ui/commands/registry.js +0 -73
@@ -0,0 +1,25 @@
1
+ /** The full-screen read-only view of one output source.
2
+ *
3
+ * A reader opens this to watch work that is not their conversation — a verifier's session, a subagent
4
+ * child, a local `!` run — without selecting it. The application follows the source and hands over laid
5
+ * out rows; this only draws them, so nothing here can write to what it is showing.
6
+ */
7
+ import { type DOMElement } from 'ink';
8
+ import type { RefObject } from 'react';
9
+ import type { HistoryRow, OutputSource } from '../../contracts.ts';
10
+ /** Draw one read-only source: its header, its content viewport, and the scroll line.
11
+ * @param props - The source, its already-laid-out rows and the reader's position in them.
12
+ * @returns The panel, filling the body area it is given.
13
+ */
14
+ export declare function PeekPanel({ source, error, rows, position, total, width, now, boxRef }: {
15
+ source: OutputSource;
16
+ error?: string;
17
+ rows: HistoryRow[];
18
+ /** Rows scrolled back from the newest row; 0 is the live end. */
19
+ position: number;
20
+ /** Rows the source has in total, so the footer can place the window. */
21
+ total: number;
22
+ width: number;
23
+ now: number;
24
+ boxRef: RefObject<DOMElement | null>;
25
+ }): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** The full-screen read-only view of one output source.
3
+ *
4
+ * A reader opens this to watch work that is not their conversation — a verifier's session, a subagent
5
+ * child, a local `!` run — without selecting it. The application follows the source and hands over laid
6
+ * out rows; this only draws them, so nothing here can write to what it is showing.
7
+ */
8
+ import { Box, Text } from 'ink';
9
+ import { HistoryViewport } from "../chat/history-view.js";
10
+ import { elapsedTime } from "../chat/status.js";
11
+ import { useTheme } from "../theme/index.js";
12
+ /** One source's identity line: what it is, who made it, how it is doing. */
13
+ function metaLine(source, now) {
14
+ const parts = [
15
+ source.detail ?? (source.kind === 'session' ? 'session' : 'local process'),
16
+ source.state,
17
+ source.parentSessionId === undefined ? undefined : `from ${source.parentSessionId}`,
18
+ source.startedAt === undefined ? undefined
19
+ : source.state === 'running' ? `started ${new Date(source.startedAt).toLocaleTimeString()}`
20
+ : `ran ${elapsedTime((source.endedAt ?? now) - source.startedAt)}`,
21
+ ];
22
+ return parts.filter((part) => part !== undefined).join(' · ');
23
+ }
24
+ /** Draw one read-only source: its header, its content viewport, and the scroll line.
25
+ * @param props - The source, its already-laid-out rows and the reader's position in them.
26
+ * @returns The panel, filling the body area it is given.
27
+ */
28
+ export function PeekPanel({ source, error, rows, position, total, width, now, boxRef }) {
29
+ const theme = useTheme();
30
+ const end = Math.max(0, total - position);
31
+ const first = Math.max(1, end - rows.length + 1);
32
+ return _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.accent, wrap: "truncate-end", children: source.label }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [source.id, " \u00B7 ", metaLine(source, now)] }), error !== undefined && _jsx(Text, { color: theme.colors.error, wrap: "truncate-end", children: error }), _jsx(Text, { dimColor: true, children: '─'.repeat(Math.max(0, width - 4)) }), _jsx(Box, { ref: boxRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: rows.length === 0
33
+ ? _jsx(Text, { color: theme.colors.muted, children: source.state === 'running' ? 'No output yet.' : 'This source has no output.' })
34
+ : _jsx(HistoryViewport, { rows: rows }) }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: total === 0 ? 'Esc closes' : `Esc closes · ↑ ↓ / PgUp PgDn / wheel scroll · lines ${first}–${end} of ${total}` })] });
35
+ }
@@ -19,6 +19,8 @@ export interface Choice {
19
19
  /** Trailing column, right-aligned and truncated from its start so a path keeps its tail. */
20
20
  detail?: string;
21
21
  action(): void;
22
+ /** `e` edits the row's own content, on surfaces that keep editable entries. */
23
+ edit?(): void;
22
24
  remove?(): void;
23
25
  }
24
26
  /** Keyboard-driven list with paging and optional removal.
@@ -3,7 +3,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from 'react';
4
4
  import { Box, Text, useInput, useStdin, useStdout } from 'ink';
5
5
  import stringWidth from 'string-width';
6
- import { safeText } from "../../transport/wire.js";
6
+ import { safeText } from "../../text.js";
7
7
  import { useCopyMode } from "../copy-mode.js";
8
8
  import { useTheme } from "../theme/index.js";
9
9
  /** Display width of one cell list, so wide characters count as two columns. */
@@ -34,6 +34,8 @@ export function Picker({ choices, enabled, canSelect, pageSize = 12, hint, width
34
34
  setSelected(Math.max(0, current - 1));
35
35
  else if (key.downArrow)
36
36
  setSelected(Math.min(choices.length - 1, current + 1));
37
+ else if (_input === 'e' && !key.ctrl && !key.meta)
38
+ choices[current]?.edit?.();
37
39
  else if (_input === 'd' && !key.ctrl && !key.meta || key.delete && /^\x1b\[3(?:;\d+)?~$/.test(rawKey.current))
38
40
  choices[current]?.remove?.();
39
41
  else if (key.return)
@@ -66,5 +68,5 @@ export function Picker({ choices, enabled, canSelect, pageSize = 12, hint, width
66
68
  if (!table || !isTableRow)
67
69
  return _jsxs(Text, { color: isCurrent ? theme.accent : undefined, wrap: "truncate-end", children: [cursor, safeText(choice.label)] }, choice.key);
68
70
  return _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: titleColumn, flexShrink: 0, children: _jsxs(Text, { color: isCurrent ? theme.accent : undefined, wrap: "truncate-end", children: [cursor, safeText(choice.title ?? choice.label)] }) }), _jsx(Box, { width: cellColumn, flexShrink: 0, children: _jsx(Text, { wrap: "truncate-end", children: choice.cells?.map((cell, position) => _jsx(Text, { color: cell.color, bold: cell.bold, children: safeText(cell.text) }, position)) }) }), detailShown && choice.detail !== undefined && _jsx(Box, { width: detailColumn, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, wrap: "truncate-start", children: safeText(choice.detail) }) })] }, choice.key);
69
- }), _jsx(Text, { dimColor: true, children: hint ?? `↑ ↓ select · Enter open${choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : ''} · Ctrl+C stop / exit` })] });
71
+ }), _jsx(Text, { dimColor: true, children: hint ?? `↑ ↓ select · Enter open${choices.some(choice => choice.edit) ? ' · e edit' : ''}${choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : ''} · Ctrl+C stop / exit` })] });
70
72
  }
@@ -8,9 +8,19 @@ export declare function isMouseReport(raw: string): boolean;
8
8
  * @returns Positive for older history, negative for newer history, or zero.
9
9
  */
10
10
  export declare function wheelDirection(raw: string): number;
11
+ /** One unmodified left press, in the terminal's own 1-based cells. */
12
+ export interface MousePress {
13
+ column: number;
14
+ row: number;
15
+ }
16
+ /** Decode an unmodified left press; releases, motion and modified presses are ignored.
17
+ * @param raw - One complete SGR mouse report.
18
+ * @returns The pressed cell, or undefined.
19
+ */
20
+ export declare function mousePress(raw: string): MousePress | undefined;
11
21
  /** Enable cell-based mouse reports for this mount and restore normal terminal behavior on exit.
12
22
  * @param scroll - Current transcript scrolling callback.
13
23
  * @param enabled - False restores native terminal selection while display updates are paused.
14
- * @param select - Left press callback that can freeze the display and release mouse capture.
24
+ * @param press - Left-press callback, given the terminal cell it landed on.
15
25
  */
16
- export declare function useMouseWheel(scroll: (direction: number) => void, enabled?: boolean, select?: () => void): void;
26
+ export declare function useMouseWheel(scroll: (direction: number) => void, enabled?: boolean, press?: (cell: MousePress) => void): void;
@@ -1,4 +1,4 @@
1
- /** SGR mouse reporting, shared by transcript scrolling and input suppression. */
1
+ /** SGR mouse reporting, shared by transcript scrolling, row clicks and input suppression. */
2
2
  import { useEffect, useRef } from 'react';
3
3
  import { useStdin, useStdout } from 'ink';
4
4
  /** Recognize complete SGR reports so clicks and wheel bytes never become prompt text.
@@ -18,18 +18,28 @@ export function wheelDirection(raw) {
18
18
  const base = button & ~28;
19
19
  return base === 64 ? 1 : base === 65 ? -1 : 0;
20
20
  }
21
+ /** Decode an unmodified left press; releases, motion and modified presses are ignored.
22
+ * @param raw - One complete SGR mouse report.
23
+ * @returns The pressed cell, or undefined.
24
+ */
25
+ export function mousePress(raw) {
26
+ const match = /^\x1b\[<0;(\d+);(\d+)M$/.exec(raw);
27
+ if (!match)
28
+ return undefined;
29
+ return { column: Number(match[1]), row: Number(match[2]) };
30
+ }
21
31
  /** Enable cell-based mouse reports for this mount and restore normal terminal behavior on exit.
22
32
  * @param scroll - Current transcript scrolling callback.
23
33
  * @param enabled - False restores native terminal selection while display updates are paused.
24
- * @param select - Left press callback that can freeze the display and release mouse capture.
34
+ * @param press - Left-press callback, given the terminal cell it landed on.
25
35
  */
26
- export function useMouseWheel(scroll, enabled = true, select) {
36
+ export function useMouseWheel(scroll, enabled = true, press) {
27
37
  const { internal_eventEmitter } = useStdin();
28
38
  const { stdout } = useStdout();
29
39
  const callback = useRef(scroll);
30
40
  callback.current = scroll;
31
- const selection = useRef(select);
32
- selection.current = select;
41
+ const onPress = useRef(press);
42
+ onPress.current = press;
33
43
  useEffect(() => {
34
44
  if (!enabled)
35
45
  return;
@@ -37,8 +47,11 @@ export function useMouseWheel(scroll, enabled = true, select) {
37
47
  const direction = wheelDirection(raw);
38
48
  if (direction)
39
49
  callback.current(direction);
40
- else if (/^\x1b\[<0;\d+;\d+M$/.test(raw))
41
- selection.current?.();
50
+ else {
51
+ const cell = mousePress(raw);
52
+ if (cell !== undefined)
53
+ onPress.current?.(cell);
54
+ }
42
55
  };
43
56
  internal_eventEmitter.on('input', onInput);
44
57
  if (stdout.isTTY)
@@ -1,4 +1,4 @@
1
- import type { FileReference } from '../../session/references.ts';
1
+ import type { FileReference } from '../../contracts.ts';
2
2
  /** Candidates for the unfinished reference at the end of the draft.
3
3
  * @param props - Lookup result and the highlighted candidate.
4
4
  * @returns The completion menu.
@@ -0,0 +1,7 @@
1
+ /** Presentation helpers for the status and cost panels. */
2
+ import type { CostTotal } from '../../contracts.ts';
3
+ /** Render a cost total with a marker while any record behind it is unpriced.
4
+ * @param total - Folded session or day total.
5
+ * @returns The estimate, prefixed `~` because it is derived from usage, not an invoice.
6
+ */
7
+ export declare function costText(total: CostTotal): string;
@@ -0,0 +1,5 @@
1
+ /** Render a cost total with a marker while any record behind it is unpriced.
2
+ * @param total - Folded session or day total.
3
+ * @returns The estimate, prefixed `~` because it is derived from usage, not an invoice.
4
+ */
5
+ export function costText(total) { return `~¥${total.amount.toFixed(4)}${total.unknown ? '*' : ''}`; }
@@ -1,8 +1,13 @@
1
- import type { RowKind } from '../../session/history.ts';
1
+ import type { RowKind } from '../../contracts.ts';
2
2
  /** Themes provide semantic roles so components never depend on palette-specific color names. */
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itookit/dsht",
3
- "version": "0.3.7",
3
+ "version": "0.5.1",
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": {
@@ -27,7 +27,7 @@
27
27
  "scripts": {
28
28
  "start": "tsx src/cli/index.ts",
29
29
  "start:profile": "node -e \"require('node:fs').mkdirSync('.diagnostics',{recursive:true})\" && NODE_OPTIONS=\"--expose-gc --heapsnapshot-signal=SIGUSR2 --diagnostic-dir=.diagnostics\" tsx src/cli/index.ts",
30
- "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
30
+ "build": "npm run build:prompts && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
31
31
  "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
32
32
  "test": "tsx --test tests/*/*.test.ts tests/*/*.test.tsx",
33
33
  "test:terminal": "node scripts/test/terminal.mjs",
@@ -35,7 +35,8 @@
35
35
  "prepack": "npm run build",
36
36
  "prepublishOnly": "npm run typecheck && npm run test:terminal",
37
37
  "bench:input": "tsx scripts/benchmarks/input.tsx",
38
- "bench:history": "node --import tsx scripts/benchmarks/history.tsx"
38
+ "bench:history": "node --import tsx scripts/benchmarks/history.tsx",
39
+ "build:prompts": "tsx scripts/build/loop-prompts.mts"
39
40
  },
40
41
  "keywords": [
41
42
  "deepseek",
@@ -84,6 +85,7 @@
84
85
  "@types/ws": "^8.18.1",
85
86
  "ink-testing-library": "^4.0.0",
86
87
  "tsx": "^4.22.0",
87
- "typescript": "^5.9.3"
88
+ "typescript": "^5.9.3",
89
+ "yaml": "^2.9.1"
88
90
  }
89
91
  }
@@ -1,99 +0,0 @@
1
- import type { State } from '../../state.ts';
2
- /** One classified submission; every side effect stays with the caller. */
3
- export type Submission = {
4
- kind: 'ignore';
5
- } | {
6
- kind: 'reference';
7
- } | {
8
- kind: 'copy';
9
- } | {
10
- kind: 'quit';
11
- } | {
12
- kind: 'panel';
13
- panel: 'cost' | 'status' | 'help';
14
- } | {
15
- kind: 'remove';
16
- target: 'workspace' | 'session';
17
- query: string;
18
- } | {
19
- kind: 'navigate';
20
- target: 'workspace' | 'session';
21
- query?: string;
22
- } | {
23
- kind: 'path';
24
- value: string;
25
- } | {
26
- kind: 'latest';
27
- } | {
28
- kind: 'models';
29
- args: string[];
30
- } | {
31
- kind: 'queue';
32
- } | {
33
- kind: 'newSession';
34
- } | {
35
- kind: 'history';
36
- query: string;
37
- } | {
38
- kind: 'sessionSearch';
39
- command: '/ssearch' | '/wsearch';
40
- query: string;
41
- } | {
42
- kind: 'historySearch';
43
- query: string;
44
- } | {
45
- kind: 'think';
46
- target: string;
47
- } | {
48
- kind: 'older';
49
- } | {
50
- kind: 'compact';
51
- } | {
52
- kind: 'cancel';
53
- } | {
54
- kind: 'approval';
55
- allowed: boolean;
56
- } | {
57
- kind: 'hostCommand';
58
- line: string;
59
- } | {
60
- kind: 'export';
61
- destination?: string;
62
- } | {
63
- kind: 'exportHtml';
64
- destination?: string;
65
- } | {
66
- kind: 'coredump';
67
- tag?: string;
68
- } | {
69
- kind: 'answer';
70
- text: string;
71
- } | {
72
- kind: 'error';
73
- message: string;
74
- } | {
75
- kind: 'prompt';
76
- text: string;
77
- };
78
- /** UI facts the classification reads; everything else stays with the caller. */
79
- export interface SubmissionContext {
80
- /** Completion menu owns the draft while open. */
81
- referenceOpen: boolean;
82
- /** Copy mode ignores submissions entirely. */
83
- copyMode: boolean;
84
- /** A question or approval of the selected session is pending. */
85
- pending: boolean;
86
- /** The pending interaction is a question. */
87
- question: boolean;
88
- screen: State['screen'];
89
- }
90
- /** Classify one composer draft without performing any of its effects.
91
- *
92
- * The order of the checks is the command precedence: reference completion, copy mode, the panels
93
- * that toggle in place, navigation and removal, then the session and host commands, then answers,
94
- * then a plain prompt.
95
- * @param raw - Draft exactly as submitted.
96
- * @param context - Current UI facts.
97
- * @returns The action to perform.
98
- */
99
- export declare function classifySubmission(raw: string, context: SubmissionContext): Submission;
@@ -1,126 +0,0 @@
1
- /** Classify one submitted composer line into the action the terminal performs. */
2
- import { navigationCommand } from "../../session/navigation.js";
3
- /** Classify one composer draft without performing any of its effects.
4
- *
5
- * The order of the checks is the command precedence: reference completion, copy mode, the panels
6
- * that toggle in place, navigation and removal, then the session and host commands, then answers,
7
- * then a plain prompt.
8
- * @param raw - Draft exactly as submitted.
9
- * @param context - Current UI facts.
10
- * @returns The action to perform.
11
- */
12
- export function classifySubmission(raw, context) {
13
- if (context.referenceOpen)
14
- return { kind: 'reference' };
15
- if (context.copyMode)
16
- return { kind: 'ignore' };
17
- const value = raw.trim();
18
- if (!value)
19
- return { kind: 'ignore' };
20
- if (value === '/copy')
21
- return { kind: 'copy' };
22
- if (value === '/quit')
23
- return { kind: 'quit' };
24
- if (value === '/cost')
25
- return { kind: 'panel', panel: 'cost' };
26
- if (value === '/status')
27
- return { kind: 'panel', panel: 'status' };
28
- if (value === '/help')
29
- return { kind: 'panel', panel: 'help' };
30
- const navigation = navigationCommand(value);
31
- if (navigation && /^--(?:delete|archive)(?:\s|$)/.test(navigation.query ?? '')) {
32
- const query = navigation.query.replace(/^--(?:delete|archive)\s*/, '');
33
- if (!query)
34
- return { kind: 'error', message: 'Specify the name or ID to remove' };
35
- return { kind: 'remove', target: navigation.kind, query };
36
- }
37
- if (navigation)
38
- return { kind: 'navigate', target: navigation.kind, query: navigation.query };
39
- if (context.screen === 'path')
40
- return { kind: 'path', value };
41
- if (value === '/latest')
42
- return { kind: 'latest' };
43
- if (/^\/model(?: |$)/.test(value)) {
44
- if (context.screen !== 'chat')
45
- return { kind: 'error', message: 'Select a session first' };
46
- const args = value.split(/\s+/).slice(1);
47
- if (args.length && (args.length < 2 || args.length > 3))
48
- return { kind: 'error', message: 'Use /model [provider model [effort]]' };
49
- return { kind: 'models', args };
50
- }
51
- if (value === '/queue') {
52
- if (context.screen !== 'chat')
53
- return { kind: 'error', message: 'Select a session first' };
54
- if (context.pending)
55
- return { kind: 'error', message: 'Answer the pending question or approval first' };
56
- return { kind: 'queue' };
57
- }
58
- if (value === '/new')
59
- return { kind: 'newSession' };
60
- if (value === '/history' || value.startsWith('/history ')) {
61
- if (context.screen !== 'chat')
62
- return { kind: 'error', message: 'Select a session first' };
63
- return { kind: 'history', query: value.slice(8).trim() };
64
- }
65
- if (/^\/(?:search|ssearch|wsearch)(?: |$)/.test(value)) {
66
- const [command, ...words] = value.split(' ');
67
- const query = words.join(' ').trim();
68
- if (!query)
69
- return { kind: 'error', message: `Use ${command} <text>` };
70
- return command === '/search' ? { kind: 'historySearch', query }
71
- : { kind: 'sessionSearch', command: command, query };
72
- }
73
- if (/^\/think(?: |$)/.test(value)) {
74
- if (context.screen !== 'chat')
75
- return { kind: 'error', message: 'Select a session first' };
76
- return { kind: 'think', target: value.slice(6).trim() };
77
- }
78
- if (value === '/older')
79
- return { kind: 'older' };
80
- if (/^\/compact(?: |$)/.test(value)) {
81
- if (context.screen !== 'chat')
82
- return { kind: 'error', message: 'Select a session first' };
83
- if (value !== '/compact')
84
- return { kind: 'error', message: 'Use /compact (no arguments)' };
85
- return { kind: 'compact' };
86
- }
87
- if (value === '/cancel')
88
- return { kind: 'cancel' };
89
- if (value === '/allow')
90
- return { kind: 'approval', allowed: true };
91
- if (value === '/deny')
92
- return { kind: 'approval', allowed: false };
93
- if (/^\/(?:plan|goal|permission|feedback)(?:\s|$)/.test(value)) {
94
- if (context.screen !== 'chat')
95
- return { kind: 'error', message: 'Select a session first' };
96
- if (context.pending)
97
- return { kind: 'error', message: 'Answer the pending question or approval first' };
98
- return { kind: 'hostCommand', line: value };
99
- }
100
- if (/^\/export(?:\s|$)/.test(value)) {
101
- if (context.screen !== 'chat')
102
- return { kind: 'error', message: 'Select a session first' };
103
- const destination = value.slice(7).trim().replace(/^(["'])(.*)\1$/, '$2');
104
- return { kind: 'export', ...(destination ? { destination } : {}) };
105
- }
106
- if (/^\/export-html(?:\s|$)/.test(value)) {
107
- if (context.screen !== 'chat')
108
- return { kind: 'error', message: 'Select a session first' };
109
- const destination = value.slice(12).trim().replace(/^(["'])(.*)\1$/, '$2');
110
- return { kind: 'exportHtml', ...(destination ? { destination } : {}) };
111
- }
112
- if (/^\/coredump(?:\s|$)/.test(value)) {
113
- // A diagnostic of this client's own heap needs neither a session nor a connected host.
114
- const tag = value.slice(9).trim().replace(/^(["'])(.*)\1$/, '$2');
115
- return { kind: 'coredump', ...(tag ? { tag } : {}) };
116
- }
117
- if (context.question)
118
- return { kind: 'answer', text: value };
119
- if (context.pending)
120
- return { kind: 'error', message: 'Answer the approval with /allow or /deny' };
121
- if (value.startsWith('/'))
122
- return { kind: 'error', message: 'Unknown command. Use /help.' };
123
- if (context.screen !== 'chat')
124
- return { kind: 'error', message: 'Choose a session or type /ws or /resume' };
125
- return { kind: 'prompt', text: value };
126
- }
@@ -1,33 +0,0 @@
1
- /** Slash-command catalog shared by completion, `/help` and the submission router. */
2
- /** One slash command advertised by completion and `/help`. */
3
- export interface CommandHint {
4
- /** Slash command as typed without arguments. */
5
- command: string;
6
- /** Argument hint shown after the command; absent when it takes none. */
7
- usage?: string;
8
- /** One-line action description shown by `/help`. */
9
- description: string;
10
- }
11
- /** Command discovery catalog shared by Tab completion and the `/help` panel. */
12
- export declare const COMMAND_HINTS: readonly CommandHint[];
13
- /** Command names only, in catalog order. */
14
- export declare const COMMANDS: string[];
15
- /** Command column text per hint, aligned in the `/help` panel. */
16
- export declare const COMMAND_LABELS: string[];
17
- /** Widest command column, so descriptions start on one column. */
18
- export declare const COMMAND_LABEL_WIDTH: number;
19
- /** Longest common prefix of the candidate commands, so Tab can extend an ambiguous draft.
20
- * @param values - Command candidates.
21
- * @returns The shared leading prefix.
22
- */
23
- export declare function commonPrefix(values: string[]): string;
24
- /** Complete the leading slash command; an ambiguous draft extends to the shared prefix.
25
- * @param input - Current composer draft.
26
- * @returns The completed draft, or undefined when nothing can be completed.
27
- */
28
- export declare function completeCommand(input: string): string | undefined;
29
- /** Commands matching the current draft, shown under the composer.
30
- * @param input - Current composer draft.
31
- * @returns Candidate command names.
32
- */
33
- export declare function suggestedCommands(input: string): string[];
@@ -1,73 +0,0 @@
1
- /** Slash-command catalog shared by completion, `/help` and the submission router. */
2
- /** Command discovery catalog shared by Tab completion and the `/help` panel. */
3
- export const COMMAND_HINTS = [
4
- { command: '/ws', usage: '[name or ID]', description: 'List/switch workspaces; --delete name removes registration' },
5
- { command: '/resume', usage: '[title or ID]', description: 'List/switch sessions; --delete ID archives with confirmation' },
6
- { command: '/model', usage: '[provider model [effort]]', description: 'Choose a model and reasoning effort for subsequent requests' },
7
- { command: '/new', description: 'Create a session in the selected workspace' },
8
- { command: '/copy', description: 'Freeze for native selection; Esc resumes (Ctrl+S shortcut)' },
9
- { command: '/latest', description: 'Return to the live conversation' },
10
- { command: '/older', description: 'Load earlier history' },
11
- { command: '/history', usage: '[text]', description: 'List your prompts, optionally filtered' },
12
- { command: '/search', usage: 'text', description: 'Search history page by page and open a match' },
13
- { command: '/ssearch', usage: 'text', description: 'Search sessions in the current workspace' },
14
- { command: '/wsearch', usage: 'text', description: 'Search sessions across all workspaces' },
15
- { command: '/compact', description: 'Compact older history while the session is idle' },
16
- { command: '/cancel', description: 'Cancel the active turn' },
17
- { command: '/queue', description: 'View and remove pending input' },
18
- { command: '/plan', usage: '[off|message]', description: 'Enter or leave host plan mode' },
19
- { command: '/goal', usage: '[action|objective]', description: 'View or manage the host task goal' },
20
- { command: '/permission', usage: '[preset]', description: 'View or switch the host permission preset' },
21
- { command: '/feedback', usage: 'text', description: 'Record feedback about the session' },
22
- { command: '/export', usage: '[local.zip]', description: 'Save the session log ZIP to a new local file' },
23
- { command: '/export-html', usage: '[local.html]', description: 'Save loaded conversation with diagrams and math as offline HTML' },
24
- { command: '/coredump', usage: '[tag]', description: 'Write a V8 heap snapshot for memory diagnosis' },
25
- { command: '/allow', description: 'Approve the pending request once' },
26
- { command: '/deny', description: 'Reject the pending request' },
27
- { command: '/status', description: 'Show full session status details' },
28
- { command: '/cost', description: 'Show cost estimates and refresh usage' },
29
- { command: '/think', usage: '[seq or live]', description: 'Inspect reasoning with user prompt summaries' },
30
- { command: '/help', description: 'Show this command list' },
31
- { command: '/quit', description: 'Exit dsht' },
32
- ];
33
- /** Command names only, in catalog order. */
34
- export const COMMANDS = COMMAND_HINTS.map(hint => hint.command);
35
- /** Command column text per hint, aligned in the `/help` panel. */
36
- export const COMMAND_LABELS = COMMAND_HINTS.map(hint => hint.usage === undefined ? hint.command : `${hint.command} ${hint.usage}`);
37
- /** Widest command column, so descriptions start on one column. */
38
- export const COMMAND_LABEL_WIDTH = Math.max(...COMMAND_LABELS.map(label => label.length)) + 2;
39
- /** Longest common prefix of the candidate commands, so Tab can extend an ambiguous draft.
40
- * @param values - Command candidates.
41
- * @returns The shared leading prefix.
42
- */
43
- export function commonPrefix(values) {
44
- let prefix = values[0] ?? '';
45
- for (const value of values) {
46
- let index = 0;
47
- while (index < prefix.length && index < value.length && prefix[index] === value[index])
48
- index++;
49
- prefix = prefix.slice(0, index);
50
- }
51
- return prefix;
52
- }
53
- /** Complete the leading slash command; an ambiguous draft extends to the shared prefix.
54
- * @param input - Current composer draft.
55
- * @returns The completed draft, or undefined when nothing can be completed.
56
- */
57
- export function completeCommand(input) {
58
- if (!input.startsWith('/') || input.includes(' '))
59
- return undefined;
60
- const matches = COMMANDS.filter(command => command.startsWith(input));
61
- const only = matches.length === 1 ? matches[0] : undefined;
62
- if (only !== undefined)
63
- return `${only} `;
64
- const prefix = commonPrefix(matches);
65
- return prefix.length > input.length ? prefix : undefined;
66
- }
67
- /** Commands matching the current draft, shown under the composer.
68
- * @param input - Current composer draft.
69
- * @returns Candidate command names.
70
- */
71
- export function suggestedCommands(input) {
72
- return COMMANDS.filter(command => input.startsWith('/') && !input.includes(' ') && command.startsWith(input));
73
- }