@itookit/dsht 0.3.8 → 0.5.2

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 (166) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +33 -12
  3. package/README.zh.md +35 -14
  4. package/dist/catalog/controller.d.ts +26 -6
  5. package/dist/catalog/controller.js +73 -45
  6. package/dist/catalog/index.d.ts +1 -0
  7. package/dist/cli/dsht.js +206 -18
  8. package/dist/cli/startup.d.ts +40 -0
  9. package/dist/cli/startup.js +314 -0
  10. package/dist/cli/trace-summary.d.ts +78 -0
  11. package/dist/cli/trace-summary.js +241 -0
  12. package/dist/cli/verifier.d.ts +64 -0
  13. package/dist/cli/verifier.js +265 -0
  14. package/dist/contracts.d.ts +359 -0
  15. package/dist/contracts.js +1 -0
  16. package/dist/controller/commands.d.ts +47 -0
  17. package/dist/controller/commands.js +322 -0
  18. package/dist/controller/connection-streams.d.ts +22 -0
  19. package/dist/controller/connection-streams.js +105 -0
  20. package/dist/controller/connection.d.ts +24 -31
  21. package/dist/controller/connection.js +48 -111
  22. package/dist/controller/controller.d.ts +412 -178
  23. package/dist/controller/controller.js +713 -167
  24. package/dist/controller/foreground.d.ts +44 -0
  25. package/dist/controller/foreground.js +79 -0
  26. package/dist/controller/index.d.ts +8 -1
  27. package/dist/controller/index.js +5 -0
  28. package/dist/controller/loop-contract.d.ts +136 -0
  29. package/dist/controller/loop-contract.js +308 -0
  30. package/dist/controller/loop-coordinator.d.ts +48 -0
  31. package/dist/controller/loop-coordinator.js +647 -0
  32. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  33. package/dist/controller/loop-prompts-schema.js +144 -0
  34. package/dist/controller/loop-prompts.d.ts +55 -0
  35. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  36. package/dist/controller/loop-prompts.generated.js +185 -0
  37. package/dist/controller/loop-prompts.js +104 -0
  38. package/dist/controller/loop-protocols.d.ts +39 -0
  39. package/dist/controller/loop-protocols.js +115 -0
  40. package/dist/controller/loop.d.ts +275 -0
  41. package/dist/controller/loop.js +378 -0
  42. package/dist/controller/prompts.d.ts +54 -0
  43. package/dist/controller/prompts.js +162 -0
  44. package/dist/controller/trace-log.d.ts +45 -0
  45. package/dist/controller/trace-log.js +144 -0
  46. package/dist/controller/verifier.d.ts +130 -0
  47. package/dist/controller/verifier.js +75 -0
  48. package/dist/cost/controller.d.ts +1 -1
  49. package/dist/cost/controller.js +12 -5
  50. package/dist/cost/index.d.ts +1 -1
  51. package/dist/cost/index.js +1 -1
  52. package/dist/cost/ledger.d.ts +0 -1
  53. package/dist/cost/ledger.js +0 -1
  54. package/dist/cost/scanner.js +1 -0
  55. package/dist/json.d.ts +18 -0
  56. package/dist/json.js +19 -0
  57. package/dist/references.d.ts +25 -0
  58. package/dist/references.js +26 -0
  59. package/dist/session/connection-view.d.ts +2 -11
  60. package/dist/session/controller.d.ts +94 -105
  61. package/dist/session/controller.js +262 -536
  62. package/dist/session/history-reader.d.ts +32 -0
  63. package/dist/session/history-reader.js +170 -0
  64. package/dist/session/history.d.ts +6 -18
  65. package/dist/session/history.js +1 -24
  66. package/dist/session/index.d.ts +9 -4
  67. package/dist/session/index.js +7 -3
  68. package/dist/session/info.d.ts +20 -82
  69. package/dist/session/info.js +52 -25
  70. package/dist/session/interactions.d.ts +26 -0
  71. package/dist/session/interactions.js +75 -0
  72. package/dist/session/markdown.js +1 -1
  73. package/dist/session/math.js +1 -1
  74. package/dist/session/mutation-gate.d.ts +51 -0
  75. package/dist/session/mutation-gate.js +73 -0
  76. package/dist/session/navigation.d.ts +2 -89
  77. package/dist/session/navigation.js +2 -129
  78. package/dist/session/navigator.d.ts +47 -0
  79. package/dist/session/navigator.js +158 -0
  80. package/dist/session/peek.d.ts +38 -0
  81. package/dist/session/peek.js +103 -0
  82. package/dist/session/prompt-backfill.d.ts +23 -0
  83. package/dist/session/prompt-backfill.js +88 -0
  84. package/dist/session/references.d.ts +2 -20
  85. package/dist/session/references.js +1 -26
  86. package/dist/session/runtime.d.ts +26 -0
  87. package/dist/session/runtime.js +28 -0
  88. package/dist/session/state.d.ts +20 -0
  89. package/dist/session/state.js +1 -0
  90. package/dist/session/telemetry.d.ts +25 -17
  91. package/dist/session/telemetry.js +66 -60
  92. package/dist/session/transcript.d.ts +5 -7
  93. package/dist/session/transcript.js +2 -15
  94. package/dist/session/types.d.ts +25 -0
  95. package/dist/session/types.js +0 -1
  96. package/dist/session-title.d.ts +9 -0
  97. package/dist/session-title.js +21 -0
  98. package/dist/shell/controller.d.ts +31 -1
  99. package/dist/shell/controller.js +34 -2
  100. package/dist/shell/index.d.ts +3 -3
  101. package/dist/shell/index.js +2 -2
  102. package/dist/shell/runner.d.ts +10 -0
  103. package/dist/shell/runner.js +48 -9
  104. package/dist/slash/index.d.ts +10 -0
  105. package/dist/slash/index.js +7 -0
  106. package/dist/slash/parse.d.ts +42 -0
  107. package/dist/slash/parse.js +259 -0
  108. package/dist/slash/pipeline.d.ts +140 -0
  109. package/dist/slash/pipeline.js +115 -0
  110. package/dist/slash/registry.d.ts +88 -0
  111. package/dist/slash/registry.js +177 -0
  112. package/dist/slash/types.d.ts +126 -0
  113. package/dist/slash/types.js +1 -0
  114. package/dist/state.d.ts +16 -18
  115. package/dist/state.js +4 -3
  116. package/dist/text.d.ts +28 -0
  117. package/dist/text.js +55 -0
  118. package/dist/transport/client.d.ts +4 -3
  119. package/dist/transport/client.js +71 -25
  120. package/dist/transport/events.d.ts +104 -0
  121. package/dist/transport/events.js +149 -0
  122. package/dist/transport/wire.d.ts +9 -17
  123. package/dist/transport/wire.js +2 -27
  124. package/dist/ui/app.js +750 -550
  125. package/dist/ui/chat/header.js +1 -1
  126. package/dist/ui/chat/history-view.d.ts +1 -1
  127. package/dist/ui/chat/loop-status.d.ts +11 -0
  128. package/dist/ui/chat/loop-status.js +28 -0
  129. package/dist/ui/chat/navigation-model.d.ts +86 -0
  130. package/dist/ui/chat/navigation-model.js +107 -0
  131. package/dist/ui/chat/shell-view.d.ts +17 -2
  132. package/dist/ui/chat/shell-view.js +45 -3
  133. package/dist/ui/chat/status.d.ts +47 -3
  134. package/dist/ui/chat/status.js +65 -50
  135. package/dist/ui/chat/use-history-view.d.ts +69 -0
  136. package/dist/ui/chat/use-history-view.js +123 -0
  137. package/dist/ui/chat/viewport.d.ts +1 -1
  138. package/dist/ui/dialogs/cost.d.ts +21 -4
  139. package/dist/ui/dialogs/cost.js +7 -12
  140. package/dist/ui/dialogs/index.d.ts +22 -5
  141. package/dist/ui/dialogs/index.js +19 -3
  142. package/dist/ui/dialogs/loop.d.ts +43 -0
  143. package/dist/ui/dialogs/loop.js +224 -0
  144. package/dist/ui/dialogs/peek.d.ts +25 -0
  145. package/dist/ui/dialogs/peek.js +35 -0
  146. package/dist/ui/dialogs/picker.d.ts +2 -0
  147. package/dist/ui/dialogs/picker.js +4 -2
  148. package/dist/ui/dialogs/use-panels.d.ts +53 -0
  149. package/dist/ui/dialogs/use-panels.js +51 -0
  150. package/dist/ui/input/mouse.d.ts +12 -2
  151. package/dist/ui/input/mouse.js +20 -7
  152. package/dist/ui/input/references.d.ts +1 -1
  153. package/dist/ui/input/use-composer.d.ts +35 -0
  154. package/dist/ui/input/use-composer.js +109 -0
  155. package/dist/ui/input/use-deferred-lines.d.ts +16 -0
  156. package/dist/ui/input/use-deferred-lines.js +54 -0
  157. package/dist/ui/input/use-history-recall.d.ts +20 -0
  158. package/dist/ui/input/use-history-recall.js +47 -0
  159. package/dist/ui/status/model.d.ts +7 -0
  160. package/dist/ui/status/model.js +5 -0
  161. package/dist/ui/theme/index.d.ts +1 -1
  162. package/package.json +6 -4
  163. package/dist/ui/commands/parse.d.ts +0 -104
  164. package/dist/ui/commands/parse.js +0 -135
  165. package/dist/ui/commands/registry.d.ts +0 -33
  166. package/dist/ui/commands/registry.js +0 -73
@@ -0,0 +1,69 @@
1
+ /** Reading position, folds and asynchronous navigation belong to this mounted conversation view. */
2
+ import { type SetStateAction } from 'react';
3
+ import type { Reasoning, SessionRender, ShellBlock } from '../../contracts.ts';
4
+ interface HistorySource {
5
+ readonly version: number;
6
+ readonly memoryRevision: number;
7
+ readonly beforeSeq: number | undefined;
8
+ readonly ready: boolean;
9
+ readonly hasMore: boolean;
10
+ readonly liveAttemptKey: string | undefined;
11
+ }
12
+ interface HistoryViewOptions<S extends HistorySource> {
13
+ sessionId?: string;
14
+ source: S;
15
+ display: S;
16
+ detached: boolean;
17
+ width: number;
18
+ rows: number;
19
+ showHint: boolean;
20
+ protect: boolean;
21
+ enabled: boolean;
22
+ online: boolean;
23
+ busy: boolean;
24
+ revision: number;
25
+ blocks: readonly ShellBlock[];
26
+ current(source: S): boolean;
27
+ render(input: {
28
+ transcript: S;
29
+ width: number;
30
+ folds: ReadonlySet<number>;
31
+ liveReasoning: Reasoning;
32
+ }): SessionRender;
33
+ run<T>(work: (signal: AbortSignal) => Promise<T>, label: string): Promise<T | undefined>;
34
+ older(signal: AbortSignal, source: S): Promise<boolean>;
35
+ open(target: number, signal: AbortSignal): Promise<boolean>;
36
+ pin(value: boolean): void;
37
+ closePanels(): void;
38
+ notify(message: string): void;
39
+ }
40
+ export declare function useHistoryView<S extends HistorySource>(options: HistoryViewOptions<S>): {
41
+ layout: {
42
+ messages: import("../../contracts.ts").Message[];
43
+ offsets: Map<number, number>;
44
+ first: number | undefined;
45
+ length: number;
46
+ liveOffset: number;
47
+ viewport: (start: number, end: number) => import("../../contracts.ts").HistoryRow[];
48
+ readonly cachedRowCount: number;
49
+ readonly lines: string[];
50
+ };
51
+ merged: {
52
+ total: number;
53
+ hostRow(row: number): number;
54
+ sources: ReadonlyMap<number, string>;
55
+ viewport(start: number, end: number): import("../../contracts.ts").HistoryRow[];
56
+ };
57
+ totalRows: number;
58
+ pageSize: number;
59
+ position: number;
60
+ reasoningOverrides: ReadonlySet<number>;
61
+ liveReasoning: Reasoning;
62
+ setScroll: (value: SetStateAction<number>) => void;
63
+ setReasoningOverrides: (value: SetStateAction<ReadonlySet<number>>) => void;
64
+ setLiveReasoning: (value: SetStateAction<Reasoning>) => void;
65
+ scrollHistory: (delta: number) => void;
66
+ jumpHistory: (target: number) => Promise<void>;
67
+ historyOperation: <T>(work: (signal: AbortSignal) => Promise<T>, label?: string) => Promise<T | undefined>;
68
+ };
69
+ export {};
@@ -0,0 +1,123 @@
1
+ /** Reading position, folds and asynchronous navigation belong to this mounted conversation view. */
2
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { errorText } from "../../text.js";
4
+ import { mergeShellRuns } from "./shell-view.js";
5
+ export function useHistoryView(options) {
6
+ const [scroll, setScroll] = useState(0);
7
+ const [folds, setFolds] = useState(new Set());
8
+ const [liveReasoning, setLive] = useState('row');
9
+ const [anchor, setAnchor] = useState();
10
+ const latest = useRef(options);
11
+ latest.current = options;
12
+ const mounted = useRef(true);
13
+ const intent = useRef(0);
14
+ const jumpAbort = useRef(undefined);
15
+ const pageAbort = useRef(undefined);
16
+ const lifetime = useRef(new AbortController());
17
+ const paging = useRef(false);
18
+ const invalidate = () => { intent.current++; jumpAbort.current?.abort(); setAnchor(undefined); };
19
+ useEffect(() => {
20
+ if (lifetime.current.signal.aborted)
21
+ lifetime.current = new AbortController();
22
+ mounted.current = true;
23
+ return () => {
24
+ mounted.current = false;
25
+ intent.current++;
26
+ lifetime.current.abort();
27
+ jumpAbort.current?.abort();
28
+ pageAbort.current?.abort();
29
+ latest.current.pin(false);
30
+ };
31
+ }, []);
32
+ useEffect(() => {
33
+ invalidate();
34
+ pageAbort.current?.abort();
35
+ setScroll(0);
36
+ }, [options.source]);
37
+ useEffect(() => { setFolds(new Set()); setLive('row'); }, [options.sessionId]);
38
+ useEffect(() => { setLive('row'); }, [options.source, options.source.liveAttemptKey]);
39
+ useEffect(() => {
40
+ const first = options.display.beforeSeq;
41
+ if (first !== undefined)
42
+ setFolds(previous => {
43
+ const next = new Set([...previous].filter(seq => seq >= first));
44
+ return next.size === previous.size ? previous : next;
45
+ });
46
+ }, [options.display, options.display.memoryRevision]);
47
+ const { source, display, width } = options;
48
+ const layout = useMemo(() => options.render({ transcript: display, width, folds, liveReasoning }), [display, display.version, width, folds, liveReasoning]);
49
+ const merged = useMemo(() => mergeShellRuns(layout, options.blocks, width), [layout, options.blocks, options.revision, width]);
50
+ const pageSize = Math.max(1, options.rows - (options.showHint ? 1 : 0));
51
+ const totalRows = merged.total;
52
+ const maxScroll = Math.max(0, totalRows - pageSize);
53
+ const previousView = useRef({ source, display, count: totalRows, first: layout.first, folds, liveReasoning });
54
+ const previous = previousView.current;
55
+ const prepended = previous.first !== undefined && layout.first !== undefined && layout.first < previous.first;
56
+ const adjusted = previous.source !== source ? 0
57
+ : scroll > 0 && previous.display === display && !prepended && previous.folds === folds && previous.liveReasoning === liveReasoning
58
+ ? Math.max(0, scroll + totalRows - previous.count) : scroll;
59
+ const row = anchor?.source === source ? layout.offsets.get(anchor.target) : undefined;
60
+ const position = Math.max(0, Math.min(row === undefined ? adjusted : totalRows - pageSize - merged.hostRow(row), maxScroll));
61
+ useLayoutEffect(() => {
62
+ previousView.current = { source, display, count: totalRows, first: layout.first, folds, liveReasoning };
63
+ if (position !== scroll)
64
+ setScroll(position);
65
+ if (anchor)
66
+ setAnchor(undefined);
67
+ }, [source, display, totalRows, position, scroll, folds, liveReasoning, anchor]);
68
+ useLayoutEffect(() => { options.pin(!options.detached && (position > 0 || options.protect)); }, [options.detached, position, options.protect, source]);
69
+ function scrollTo(value) { invalidate(); setScroll(value); }
70
+ function setReasoningOverrides(value) { invalidate(); setFolds(value); }
71
+ function setLiveReasoning(value) { invalidate(); setLive(value); }
72
+ function historyOperation(work, label = 'Loading history…') {
73
+ return options.run(signal => work(AbortSignal.any([signal, lifetime.current.signal])), label);
74
+ }
75
+ function scrollHistory(delta) {
76
+ if (!options.enabled)
77
+ return;
78
+ invalidate();
79
+ const own = intent.current;
80
+ const next = Math.max(0, Math.min(maxScroll, position + delta));
81
+ setScroll(next);
82
+ if (delta <= 0 || next < maxScroll || paging.current || options.busy || !options.online || !display.ready || !display.hasMore)
83
+ return;
84
+ paging.current = true;
85
+ const abort = new AbortController();
86
+ pageAbort.current = abort;
87
+ void historyOperation(signal => options.older(AbortSignal.any([signal, abort.signal]), display)).then(accepted => {
88
+ if (accepted && mounted.current && latest.current.display === display && options.current(source) && intent.current === own) {
89
+ setScroll(position + delta);
90
+ }
91
+ }).catch(error => {
92
+ if (mounted.current && !(error instanceof Error && error.name === 'AbortError'))
93
+ latest.current.notify(errorText(error));
94
+ }).finally(() => { paging.current = false; if (pageAbort.current === abort)
95
+ pageAbort.current = undefined; });
96
+ }
97
+ async function jumpHistory(target) {
98
+ if (!options.enabled)
99
+ throw new Error('Select a session first');
100
+ invalidate();
101
+ const own = intent.current;
102
+ const abort = new AbortController();
103
+ jumpAbort.current = abort;
104
+ try {
105
+ await historyOperation(async (foreground) => {
106
+ const signal = AbortSignal.any([foreground, abort.signal]);
107
+ if (!await options.open(target, signal))
108
+ return;
109
+ signal.throwIfAborted();
110
+ if (!mounted.current || intent.current !== own || !options.current(source))
111
+ return;
112
+ setAnchor({ source, target });
113
+ latest.current.closePanels();
114
+ }, 'Jumping to a message…');
115
+ }
116
+ finally {
117
+ if (jumpAbort.current === abort)
118
+ jumpAbort.current = undefined;
119
+ }
120
+ }
121
+ return { layout, merged, totalRows, pageSize, position, reasoningOverrides: folds, liveReasoning,
122
+ setScroll: scrollTo, setReasoningOverrides, setLiveReasoning, scrollHistory, jumpHistory, historyOperation };
123
+ }
@@ -1,7 +1,7 @@
1
1
  /** Measured conversation viewport: only visible rows become React nodes. */
2
2
  import { type DOMElement } from 'ink';
3
3
  import type { RefObject } from 'react';
4
- import type { HistoryRow } from '../../session/history.ts';
4
+ import type { HistoryRow } from '../../contracts.ts';
5
5
  /** Visible conversation rows plus the scroll hint.
6
6
  * @param props - Rows, hint state, freeze identity and the measured box.
7
7
  * @returns The scrollable conversation area.
@@ -1,8 +1,25 @@
1
- import type { Controller } from '../../controller/index.ts';
1
+ import type { Coverage } from '../../contracts.ts';
2
+ /** One formatted cost line: what to show, and how exact it is. */
3
+ export interface CostLine {
4
+ text: string;
5
+ unknown: number;
6
+ records: number;
7
+ }
8
+ /** The billing panel's plain input, assembled by the composition root. */
9
+ export interface CostSource {
10
+ session?: CostLine;
11
+ today: CostLine;
12
+ scanning: boolean;
13
+ coverage: Coverage;
14
+ scannedAt?: number;
15
+ customPrices: boolean;
16
+ error?: string;
17
+ missing: readonly string[];
18
+ }
2
19
  /** Render cached totals while the independent HTTP cost scan refreshes.
3
- * @param controller - Selected session and origin-scoped cost ledger.
20
+ * @param source - Plain billing summary, absent when this run has no ledger.
4
21
  * @returns Billing panel, including unpriced models and refresh errors.
5
22
  */
6
- export declare function CostPanel({ controller }: {
7
- controller: Controller;
23
+ export declare function CostPanel({ source }: {
24
+ source?: CostSource;
8
25
  }): import("react").JSX.Element;
@@ -2,24 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Billing estimates with explicit coverage, calendar and unknown-price indicators. */
3
3
  import { useTheme } from "../theme/index.js";
4
4
  import { Box, Text } from 'ink';
5
- import { costText } from "../../cost/index.js";
6
- import { safeText } from "../../transport/wire.js";
5
+ import { safeText } from "../../text.js";
7
6
  /** Render cached totals while the independent HTTP cost scan refreshes.
8
- * @param controller - Selected session and origin-scoped cost ledger.
7
+ * @param source - Plain billing summary, absent when this run has no ledger.
9
8
  * @returns Billing panel, including unpriced models and refresh errors.
10
9
  */
11
- export function CostPanel({ controller }) {
10
+ export function CostPanel({ source }) {
12
11
  const theme = useTheme();
13
- const costs = controller.costs;
12
+ const costs = source;
14
13
  if (!costs)
15
14
  return _jsx(Text, { children: "Cost tracking is unavailable" });
16
- const id = controller.state.sessionId;
17
- const rows = [
18
- ['Session', id && costs.hasSession(id) ? costs.total(id) : undefined],
19
- ['Today', costs.today()],
20
- ];
21
- return _jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Cost \u00B7 CNY estimate \u00B7 Asia/Shanghai \u00B7 /cost closes" }), rows.map(([label, total]) => _jsxs(Text, { children: [label, ": ", total ? `${costText(total)} · ${total.unknown} unpriced / ${total.records} requests` : '?'] }, label)), _jsx(Text, { dimColor: true, children: costs.scanning ? 'Refreshing all visible sessions…'
15
+ const rows = [['Session', costs.session], ['Today', costs.today]];
16
+ return _jsxs(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Cost \u00B7 CNY estimate \u00B7 Asia/Shanghai \u00B7 /cost closes" }), rows.map(([label, total]) => _jsxs(Text, { children: [label, ": ", total ? `${total.text} · ${total.unknown} unpriced / ${total.records} requests` : '?'] }, label)), _jsx(Text, { dimColor: true, children: costs.scanning ? 'Refreshing all visible sessions…'
22
17
  : costs.coverage === 'partial' ? 'Partial totals · awaiting a complete scan'
23
18
  : costs.scannedAt ? `Last refresh: ${new Date(costs.scannedAt).toISOString()}`
24
- : 'Cached totals from the previous run' }), costs.customPrices && _jsx(Text, { color: theme.colors.context, children: "Rates come from prices.json, not the shipped table." }), _jsx(Text, { dimColor: true, children: "Recorded settlement time determines tariff; * means a subtotal is not exact. Provider invoices are authoritative." }), costs.error && _jsxs(Text, { color: theme.colors.context, children: ["Partial totals: ", safeText(costs.error)] }), costs.missing().slice(0, 6).map(reason => _jsx(Text, { color: theme.colors.context, children: safeText(reason) }, reason))] });
19
+ : 'Cached totals from the previous run' }), costs.customPrices && _jsx(Text, { color: theme.colors.context, children: "Rates come from prices.json, not the shipped table." }), _jsx(Text, { dimColor: true, children: "Recorded settlement time determines tariff; * means a subtotal is not exact. Provider invoices are authoritative." }), costs.error && _jsxs(Text, { color: theme.colors.context, children: ["Partial totals: ", safeText(costs.error)] }), costs.missing.slice(0, 6).map(reason => _jsx(Text, { color: theme.colors.context, children: safeText(reason) }, reason))] });
25
20
  }
@@ -1,7 +1,7 @@
1
- import { type ObjectValue } from '../../transport/wire.ts';
2
- import { type Message } from '../../session/transcript.ts';
3
- import type { HistorySearch, RemovalTarget } from '../../session/types.ts';
4
- import type { QueuedInput } from '../../session/telemetry.ts';
1
+ import { type ObjectValue } from '../../json.ts';
2
+ import { type Message, type SavedPrompt } from '../../contracts.ts';
3
+ import type { HistorySearch, RemovalTarget } from '../../contracts.ts';
4
+ import type { QueuedInput } from '../../contracts.ts';
5
5
  import { type Choice } from './picker.tsx';
6
6
  /** Pending host input with explicit removal.
7
7
  * @param props - Queue contents, geometry and availability.
@@ -16,6 +16,23 @@ export declare function QueueDialog({ queued, rows, width, unavailable, enabled,
16
16
  canSelect(): boolean;
17
17
  onRemove(id: string): void;
18
18
  }): import("react").JSX.Element;
19
+ /** Operator-saved shortcut prompts with choose, edit and delete.
20
+ *
21
+ * Enter hands the text to the composer rather than sending it, so the reader can adjust it first.
22
+ * @param props - Saved entries, their load error, geometry and the three row actions.
23
+ * @returns The saved-prompt picker.
24
+ */
25
+ export declare function PromptsDialog({ identity, prompts, error, width, enabled, canSelect, onChoose, onEdit, onRemove }: {
26
+ identity: string;
27
+ prompts: readonly SavedPrompt[];
28
+ error?: string;
29
+ width: number;
30
+ enabled: boolean;
31
+ canSelect(): boolean;
32
+ onChoose(text: string): void;
33
+ onEdit(prompt: SavedPrompt): void;
34
+ onRemove(id: string): void;
35
+ }): import("react").JSX.Element;
19
36
  /** Confirmation for workspace removal or session archival.
20
37
  * @param props - Reviewed target and its confirmation actions.
21
38
  * @returns The confirmation dialog.
@@ -27,7 +44,7 @@ export declare function RemovalDialog({ removal, enabled, canSelect, onCancel, o
27
44
  onCancel(): void;
28
45
  onConfirm(): void;
29
46
  }): import("react").JSX.Element;
30
- import type { ModelState } from '../../session/index.ts';
47
+ import type { ModelState } from '../../contracts.ts';
31
48
  /** Two-step model and reasoning-effort selector; the state shape is shared with the session. */
32
49
  export type { ModelState };
33
50
  /** Model routes and adapter-owned reasoning efforts.
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Modal panels and list screens rendered inside the shared composer frame. */
3
3
  import { Box, Text } from 'ink';
4
- import { array, object, safeText, string } from "../../transport/wire.js";
5
- import { toolLine } from "../../session/transcript.js";
6
- import { COMMAND_HINTS, COMMAND_LABELS, COMMAND_LABEL_WIDTH } from "../commands/registry.js";
4
+ import { array, object, string } from "../../json.js";
5
+ import { safeText } from "../../text.js";
6
+ import { toolLine } from "../../text.js";
7
+ import { COMMAND_HINTS, COMMAND_LABELS, COMMAND_LABEL_WIDTH } from "../../slash/registry.js";
7
8
  import { useTheme } from "../theme/index.js";
8
9
  import { Picker } from "./picker.js";
9
10
  /** Pending host input with explicit removal.
@@ -17,6 +18,21 @@ export function QueueDialog({ queued, rows, width, unavailable, enabled, canSele
17
18
  remove: () => onRemove(item.id),
18
19
  })), pageSize: Math.max(1, Math.min(6, rows - 12)), hint: "\u2191 \u2193 select \u00B7 Enter / d / Delete remove \u00B7 Esc close", enabled: enabled, canSelect: canSelect })] });
19
20
  }
21
+ /** Operator-saved shortcut prompts with choose, edit and delete.
22
+ *
23
+ * Enter hands the text to the composer rather than sending it, so the reader can adjust it first.
24
+ * @param props - Saved entries, their load error, geometry and the three row actions.
25
+ * @returns The saved-prompt picker.
26
+ */
27
+ export function PromptsDialog({ identity, prompts, error, width, enabled, canSelect, onChoose, onEdit, onRemove }) {
28
+ const theme = useTheme();
29
+ return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: "Saved prompts \u00B7 Esc close" }), error !== undefined && _jsx(Text, { color: theme.colors.error, wrap: "truncate-end", children: safeText(error) }), !prompts.length && _jsx(Text, { dimColor: true, children: "No saved prompts \u00B7 add one with /prompt TEXT" }), _jsx(Picker, { choices: prompts.map(prompt => ({
30
+ key: prompt.id, label: toolLine(prompt.text, width - 6),
31
+ action: () => onChoose(prompt.text),
32
+ edit: () => onEdit(prompt),
33
+ remove: () => onRemove(prompt.id),
34
+ })), hint: "\u2191 \u2193 select \u00B7 Enter use \u00B7 e edit \u00B7 d/Delete delete \u00B7 Esc close", enabled: enabled, canSelect: canSelect }, identity)] });
35
+ }
20
36
  /** Confirmation for workspace removal or session archival.
21
37
  * @param props - Reviewed target and its confirmation actions.
22
38
  * @returns The confirmation dialog.
@@ -0,0 +1,43 @@
1
+ import type { LoopLimits, LoopRecord } from '../../contracts.ts';
2
+ /** The numeric fields the form edits, in the order a run reads them. */
3
+ export type LoopField = 'from' | 'to' | 'score' | 'tries';
4
+ /** What the form hands to the runner: the four numbers and the record's variables as confirmed. */
5
+ export interface LoopRun {
6
+ limits: LoopLimits;
7
+ vars: Readonly<Record<string, string>>;
8
+ }
9
+ /** The values a run from one record would start with, which the form shows before anything is typed.
10
+ * @param record - Chosen record.
11
+ * @returns Its first round, last round and declared score and attempt budgets.
12
+ */
13
+ export declare function loopRecordDefaults(record: LoopRecord): LoopLimits;
14
+ /** The record list shown under the composer while `/loop` is being typed.
15
+ *
16
+ * It offers the same names and defaults the runner reads, so choosing from it cannot start a run
17
+ * other than the one the row describes.
18
+ * @param props - Matching records and the highlighted row.
19
+ * @returns The key line and up to six record rows.
20
+ */
21
+ export declare function LoopMenu({ records, index }: {
22
+ records: readonly LoopRecord[];
23
+ index: number;
24
+ }): import("react").JSX.Element;
25
+ /** The loop parameter form: one record's variables and defaults, editable in place, then a Start.
26
+ *
27
+ * Values are replaced by typing over them, so no flag has to be remembered and every default stays
28
+ * visible until it is changed. A record variable is free text because it is a path or a target, not a
29
+ * number; the four limits share the command line's own validation. Leaving a row — with Enter or with
30
+ * an arrow — commits what was typed, so no box needs its own confirmation and Start reads the values
31
+ * as displayed. The run starts only from the Start row, so no keystroke that edits a value can launch
32
+ * it by accident. The form stays readable and escapable even when the host is busy or gone; only Start
33
+ * is unavailable then.
34
+ * @param props - The chosen record, whether a run may start now, and the three row actions.
35
+ * @returns The form.
36
+ */
37
+ export declare function LoopDialog({ record, enabled, onStart, onBack, onClose }: {
38
+ record: LoopRecord;
39
+ enabled: boolean;
40
+ onStart(run: LoopRun): void;
41
+ onBack(): void;
42
+ onClose(): void;
43
+ }): import("react").JSX.Element;
@@ -0,0 +1,224 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** The `/loop` record list and the parameter form that confirms a chosen record's defaults. */
3
+ import { useMemo, useState } from 'react';
4
+ import { Box, Text, useInput } from 'ink';
5
+ import { validLoopOption } from "../../slash/parse.js";
6
+ import { safeText } from "../../text.js";
7
+ import { useCopyMode } from "../copy-mode.js";
8
+ import { useTheme } from "../theme/index.js";
9
+ const FIELDS = [
10
+ { field: 'from', label: 'From', hint: 'first round, 1 or more' },
11
+ { field: 'to', label: 'To', hint: 'last round, at least From' },
12
+ { field: 'score', label: 'Pass', hint: 'passing score, 0–10' },
13
+ { field: 'tries', label: 'Tries', hint: 'attempts per round, 1 or more' },
14
+ ];
15
+ /** The values a run from one record would start with, which the form shows before anything is typed.
16
+ * @param record - Chosen record.
17
+ * @returns Its first round, last round and declared score and attempt budgets.
18
+ */
19
+ export function loopRecordDefaults(record) {
20
+ return { from: 1, to: record.steps, score: record.defaultScore, tries: record.defaultTries };
21
+ }
22
+ /** The form's rows for one record: its own variables first, then the shared run limits.
23
+ *
24
+ * The record's variables lead because they are why this record's form was opened at all: the document
25
+ * under review is the decision that matters most, and it is not something the command line spells out.
26
+ * @param record - Chosen record.
27
+ * @returns The ordered rows.
28
+ */
29
+ function rowsFor(record) {
30
+ return [
31
+ { kind: 'start' },
32
+ ...Object.keys(record.vars).map(name => ({ kind: 'var', name })),
33
+ ...FIELDS.map(spec => ({ kind: 'field', spec })),
34
+ { kind: 'back' },
35
+ ];
36
+ }
37
+ /** Columns the label occupies, so the values line up whatever names a record declares.
38
+ * @param rows - Rows being rendered.
39
+ * @returns One past the longest label.
40
+ */
41
+ function labelWidth(rows) {
42
+ const longest = Math.max(0, ...rows.map(row => row.kind === 'field' ? row.spec.label.length
43
+ : row.kind === 'var' ? row.name.length : 0));
44
+ return Math.max(5, longest) + 1;
45
+ }
46
+ /** The record list shown under the composer while `/loop` is being typed.
47
+ *
48
+ * It offers the same names and defaults the runner reads, so choosing from it cannot start a run
49
+ * other than the one the row describes.
50
+ * @param props - Matching records and the highlighted row.
51
+ * @returns The key line and up to six record rows.
52
+ */
53
+ export function LoopMenu({ records, index }) {
54
+ const theme = useTheme();
55
+ const start = Math.max(0, index - 5);
56
+ return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Loop records \u00B7 \u2191 \u2193 select \u00B7 Enter confirm defaults \u00B7 Tab finish the name \u00B7 Esc close" }), records.slice(start, start + 6).map((record, offset) => {
57
+ const current = start + offset === index;
58
+ // The record's own inputs come before the artifact file: they are what the form will edit, and
59
+ // the first thing a narrow row must not lose.
60
+ const vars = Object.entries(record.vars).map(([name, value]) => `${name} ${value}`).join(' · ');
61
+ const target = vars === '' ? '' : ` · ${vars}`;
62
+ const artifact = record.artifact === undefined ? '' : ` · ${record.artifact}`;
63
+ return _jsxs(Text, { color: current ? theme.accent : undefined, wrap: "truncate-end", children: [current ? '❯ ' : ' ', record.name, " \u00B7 ", record.title, " \u00B7 ", record.steps, " rounds \u00B7 pass ", record.defaultScore, " \u00B7 \u2264", record.defaultTries, " tries", target, artifact] }, record.name);
64
+ })] });
65
+ }
66
+ /** The loop parameter form: one record's variables and defaults, editable in place, then a Start.
67
+ *
68
+ * Values are replaced by typing over them, so no flag has to be remembered and every default stays
69
+ * visible until it is changed. A record variable is free text because it is a path or a target, not a
70
+ * number; the four limits share the command line's own validation. Leaving a row — with Enter or with
71
+ * an arrow — commits what was typed, so no box needs its own confirmation and Start reads the values
72
+ * as displayed. The run starts only from the Start row, so no keystroke that edits a value can launch
73
+ * it by accident. The form stays readable and escapable even when the host is busy or gone; only Start
74
+ * is unavailable then.
75
+ * @param props - The chosen record, whether a run may start now, and the three row actions.
76
+ * @returns The form.
77
+ */
78
+ export function LoopDialog({ record, enabled, onStart, onBack, onClose }) {
79
+ const theme = useTheme();
80
+ const copyMode = useCopyMode();
81
+ const defaults = loopRecordDefaults(record);
82
+ const rows = useMemo(() => rowsFor(record), [record]);
83
+ const width = useMemo(() => labelWidth(rows), [rows]);
84
+ const [row, setRow] = useState(0);
85
+ const [values, setValues] = useState(defaults);
86
+ const [vars, setVars] = useState({ ...record.vars });
87
+ const [edit, setEdit] = useState();
88
+ const [error, setError] = useState();
89
+ const current = rows[Math.min(row, rows.length - 1)];
90
+ /** Validate and apply the selected editable row's buffer.
91
+ *
92
+ * Leaving a row takes its value with it, so no box needs its own Enter: an arrow to the next row
93
+ * commits what was typed, and Start then reads the committed values. A value that cannot be used
94
+ * keeps the cursor on its row and says why, so nothing is dropped silently.
95
+ * @returns True when it is safe to leave the row.
96
+ */
97
+ const applyEdit = () => {
98
+ if (edit === undefined || current.kind === 'start' || current.kind === 'back')
99
+ return true;
100
+ if (current.kind === 'var') {
101
+ const value = edit.trim();
102
+ if (value === '') {
103
+ setError(`${current.name}: a value is required`);
104
+ return false;
105
+ }
106
+ if (value !== vars[current.name])
107
+ setVars({ ...vars, [current.name]: value });
108
+ setEdit(undefined);
109
+ setError(undefined);
110
+ return true;
111
+ }
112
+ // Nothing typed (or the value erased) leaves the field as it was rather than reading `''` as 0.
113
+ if (edit === '') {
114
+ setEdit(undefined);
115
+ setError(undefined);
116
+ return true;
117
+ }
118
+ const parsed = Number(edit);
119
+ if (!validLoopOption(current.spec.field, parsed)) {
120
+ setError(`${current.spec.label}: ${current.spec.hint}`);
121
+ return false;
122
+ }
123
+ const next = { ...values, [current.spec.field]: parsed };
124
+ if (next.to < next.from) {
125
+ setError('To must be at least From');
126
+ return false;
127
+ }
128
+ setValues(next);
129
+ setEdit(undefined);
130
+ setError(undefined);
131
+ return true;
132
+ };
133
+ const move = (next) => {
134
+ if (!applyEdit())
135
+ return;
136
+ setRow(Math.max(0, Math.min(rows.length - 1, next)));
137
+ setEdit(undefined);
138
+ setError(undefined);
139
+ };
140
+ useInput((input, key) => {
141
+ if (key.eventType === 'release')
142
+ return;
143
+ if (key.escape) {
144
+ if (edit !== undefined) {
145
+ setEdit(undefined);
146
+ setError(undefined);
147
+ }
148
+ else
149
+ onClose();
150
+ return;
151
+ }
152
+ if (key.upArrow) {
153
+ move(row - 1);
154
+ return;
155
+ }
156
+ if (key.downArrow) {
157
+ move(row + 1);
158
+ return;
159
+ }
160
+ if (current.kind === 'var') {
161
+ const { name } = current;
162
+ if (key.backspace || key.delete) {
163
+ setEdit((edit ?? vars[name] ?? '').slice(0, -1));
164
+ setError(undefined);
165
+ return;
166
+ }
167
+ // A variable is a path or a target, so anything printable goes in; control bytes never do.
168
+ const typed = input.replace(/[\u0000-\u001f\u007f]/g, '');
169
+ if (typed !== '' && !key.ctrl && !key.meta) {
170
+ setEdit(edit === undefined ? typed : edit + typed);
171
+ setError(undefined);
172
+ return;
173
+ }
174
+ if (key.return)
175
+ move(row + 1);
176
+ return;
177
+ }
178
+ if (current.kind === 'field') {
179
+ const { field } = current.spec;
180
+ if (key.backspace || key.delete) {
181
+ setEdit((edit ?? String(values[field])).slice(0, -1));
182
+ setError(undefined);
183
+ return;
184
+ }
185
+ if (/^[0-9.]$/.test(input)) {
186
+ setEdit(edit === undefined ? input : edit + input);
187
+ setError(undefined);
188
+ return;
189
+ }
190
+ if (key.return)
191
+ move(row + 1);
192
+ return;
193
+ }
194
+ if (!key.return)
195
+ return;
196
+ if (current.kind === 'back') {
197
+ onBack();
198
+ return;
199
+ }
200
+ if (!enabled) {
201
+ setError('Cannot start: the host is busy or offline');
202
+ return;
203
+ }
204
+ if (values.to < values.from) {
205
+ setError('To must be at least From');
206
+ return;
207
+ }
208
+ onStart({ limits: values, vars });
209
+ }, { isActive: !copyMode });
210
+ return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Run loop record \u00B7 ", safeText(record.name), " \u00B7 Esc close"] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [record.steps, " rounds", record.artifact === undefined ? '' : ` · ${safeText(record.artifact)}`, " \u00B7 \u2191 \u2193 to a row, then type"] }), rows.map((item, index) => {
211
+ const selected = index === row;
212
+ const cursor = selected ? '❯ ' : ' ';
213
+ if (item.kind === 'start' || item.kind === 'back')
214
+ return _jsxs(Text, { color: selected ? theme.accent : undefined, children: [cursor, item.kind === 'start' ? 'Start run' : '← Choose another record'] }, item.kind);
215
+ if (item.kind === 'var') {
216
+ const value = vars[item.name] ?? '';
217
+ const shown = selected && edit !== undefined ? edit : value;
218
+ return _jsxs(Text, { color: selected ? theme.accent : undefined, children: [cursor, item.name.padEnd(width), selected && edit !== undefined ? _jsx(Text, { inverse: true, children: shown || ' ' }) : shown, value === record.vars[item.name] ? null : _jsxs(Text, { dimColor: true, children: [" \u00B7 default ", record.vars[item.name]] })] }, `var:${item.name}`);
219
+ }
220
+ const { field, label } = item.spec;
221
+ const shown = selected && edit !== undefined ? edit : String(values[field]);
222
+ return _jsxs(Text, { color: selected ? theme.accent : undefined, children: [cursor, label.padEnd(width), selected && edit !== undefined ? _jsx(Text, { inverse: true, children: shown || ' ' }) : shown, values[field] === defaults[field] ? null : _jsxs(Text, { dimColor: true, children: [" \u00B7 default ", defaults[field]] })] }, `field:${field}`);
223
+ }), error !== undefined && _jsx(Text, { color: theme.colors.error, children: safeText(error) }), _jsx(Text, { dimColor: true, children: "Enter edit a value / start \u00B7 type to replace \u00B7 Esc close" })] });
224
+ }
@@ -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;