@itookit/dsht 0.3.8 → 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 (130) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +30 -11
  3. package/README.zh.md +30 -11
  4. package/dist/cli/dsht.js +203 -18
  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 +616 -166
  18. package/dist/controller/controller.js +1395 -146
  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 +73 -72
  49. package/dist/session/controller.js +185 -209
  50. package/dist/session/history.d.ts +6 -18
  51. package/dist/session/history.js +1 -24
  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 +31 -1
  77. package/dist/shell/controller.js +34 -2
  78. package/dist/shell/index.d.ts +3 -3
  79. package/dist/shell/index.js +2 -2
  80. package/dist/shell/runner.d.ts +10 -0
  81. package/dist/shell/runner.js +48 -9
  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 +856 -441
  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/loop-status.d.ts +11 -0
  102. package/dist/ui/chat/loop-status.js +28 -0
  103. package/dist/ui/chat/navigation-model.d.ts +86 -0
  104. package/dist/ui/chat/navigation-model.js +107 -0
  105. package/dist/ui/chat/shell-view.d.ts +15 -2
  106. package/dist/ui/chat/shell-view.js +37 -3
  107. package/dist/ui/chat/status.d.ts +47 -3
  108. package/dist/ui/chat/status.js +65 -50
  109. package/dist/ui/chat/viewport.d.ts +1 -1
  110. package/dist/ui/dialogs/cost.d.ts +21 -4
  111. package/dist/ui/dialogs/cost.js +7 -12
  112. package/dist/ui/dialogs/index.d.ts +22 -5
  113. package/dist/ui/dialogs/index.js +19 -3
  114. package/dist/ui/dialogs/loop.d.ts +43 -0
  115. package/dist/ui/dialogs/loop.js +224 -0
  116. package/dist/ui/dialogs/peek.d.ts +25 -0
  117. package/dist/ui/dialogs/peek.js +35 -0
  118. package/dist/ui/dialogs/picker.d.ts +2 -0
  119. package/dist/ui/dialogs/picker.js +4 -2
  120. package/dist/ui/input/mouse.d.ts +12 -2
  121. package/dist/ui/input/mouse.js +20 -7
  122. package/dist/ui/input/references.d.ts +1 -1
  123. package/dist/ui/status/model.d.ts +7 -0
  124. package/dist/ui/status/model.js +5 -0
  125. package/dist/ui/theme/index.d.ts +1 -1
  126. package/package.json +6 -4
  127. package/dist/ui/commands/parse.d.ts +0 -104
  128. package/dist/ui/commands/parse.js +0 -135
  129. package/dist/ui/commands/registry.d.ts +0 -33
  130. package/dist/ui/commands/registry.js +0 -73
@@ -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;
@@ -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,4 +1,4 @@
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;