@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
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /** Fixed conversation header: session title, workspace, and the agent-preset label. */
3
3
  import { Box, Text } from 'ink';
4
- import { toolLine } from "../../session/transcript.js";
4
+ import { toolLine } from "../../text.js";
5
5
  import { Frozen } from "../frozen.js";
6
6
  import { useTheme } from "../theme/index.js";
7
7
  /** Session title and optional preset label above the divider.
@@ -1,4 +1,4 @@
1
- import type { HistoryRow } from '../../session/history.ts';
1
+ import type { HistoryRow } from '../../contracts.ts';
2
2
  /** Render a viewport with role, reasoning, tool and result colors from the selected theme.
3
3
  *
4
4
  * Rows are sibling text nodes rather than lines inside one text node: Ink measures and caches text per
@@ -12,6 +12,6 @@ import { useTheme } from "../theme/index.js";
12
12
  */
13
13
  export function HistoryViewport({ rows }) {
14
14
  const theme = useTheme();
15
- return _jsx(Box, { flexDirection: "column", children: rows.map((row, index) => _jsx(Text, { color: theme.colors[row.kind], bold: row.bold, children: row.spans?.length ? row.spans.map((span, position) => _jsx(Text, { bold: span.bold, italic: span.italic, underline: span.underline, strikethrough: span.strikethrough, inverse: span.inverse, children: span.text }, position))
15
+ return _jsx(Box, { flexDirection: "column", children: rows.map((row, index) => _jsx(Text, { bold: row.bold, color: row.highlight ? theme.shell.foreground : theme.colors[row.kind], backgroundColor: row.highlight ? theme.shell.background : undefined, children: row.spans?.length ? row.spans.map((span, position) => _jsx(Text, { bold: span.bold, italic: span.italic, underline: span.underline, strikethrough: span.strikethrough, inverse: span.inverse, children: span.text }, position))
16
16
  : row.text === '' ? ' ' : row.text }, index)) });
17
17
  }
@@ -0,0 +1,11 @@
1
+ import type { LoopProgress } from '../../contracts.ts';
2
+ /** Progress line shown above the composer.
3
+ *
4
+ * It names no command: the protocol supplies `title`, and the remaining fields are the generic
5
+ * step/attempt score shape, so a new loop protocol renders here without a UI change.
6
+ * @param props - The application-produced loop snapshot.
7
+ * @returns One dim line, brightened while the loop is still running.
8
+ */
9
+ export declare function LoopStatus({ progress }: {
10
+ progress: LoopProgress;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** One-line progress of a client-driven agent loop, for whichever protocol is running. */
3
+ import { Text } from 'ink';
4
+ import { useTheme } from "../theme/index.js";
5
+ /** Progress line shown above the composer.
6
+ *
7
+ * It names no command: the protocol supplies `title`, and the remaining fields are the generic
8
+ * step/attempt score shape, so a new loop protocol renders here without a UI change.
9
+ * @param props - The application-produced loop snapshot.
10
+ * @returns One dim line, brightened while the loop is still running.
11
+ */
12
+ export function LoopStatus({ progress }) {
13
+ const theme = useTheme();
14
+ const running = progress.phase === 'running';
15
+ // A paused run is waiting for the operator, not finished: the line says what to do about it.
16
+ const paused = progress.active && progress.phase === 'needs-human';
17
+ // A run stopped on a host request shows what the host is waiting for, not just the phase name.
18
+ const interaction = progress.interaction === undefined ? '' : ` · ${progress.interaction.kind}: ${progress.interaction.text}`;
19
+ // A run a verifier ended early says why: the reason is the only part of that verdict a reader acts on.
20
+ const exit = progress.exit === undefined ? '' : ` · ${progress.exit.reason}`;
21
+ // `passed` only claims the rounds that ran, so the line says which ones it covered.
22
+ const scope = progress.phase === 'passed' ? ` · ${progress.scope}` : '';
23
+ // What the live run waits on is controller data, not a note: a work turn is already visible as the
24
+ // session working, so only the states without a host turn of their own are named here.
25
+ const activity = running && progress.activity !== undefined && progress.activity !== 'turn'
26
+ ? ` · ${progress.activity}` : '';
27
+ return _jsxs(Text, { color: progress.active ? theme.colors.context : theme.colors.muted, children: [progress.title, progress.stepLabel === undefined ? '' : ` · ${progress.stepLabel}`, " \u00B7 step ", progress.step, "/", progress.to, " \u00B7 attempt ", progress.attempt, "/", progress.tries, " \u00B7 best ", progress.best, "/", progress.score, running ? '' : paused ? ' · needs you · /loop answer' : ` · ${progress.phase}`, activity, scope, exit, interaction, progress.note === undefined ? '' : ` · ${progress.note}`] });
28
+ }
@@ -0,0 +1,86 @@
1
+ /** Presentation model for the workspace and session lists.
2
+ *
3
+ * Marker, label and rollup text are the list screens' vocabulary, so they live next to the screens
4
+ * that render them. `session/navigation.ts` keeps only target resolution, which the domain needs.
5
+ */
6
+ import { type ObjectValue } from '../../json.ts';
7
+ /** User-visible activity of one session, most actionable first.
8
+ *
9
+ * `needs` is the only state that asks the user to do something, and it outranks the host's running
10
+ * flag because a turn waiting on an answer is running only in the mechanical sense. `blank` stays a
11
+ * marker for a session that never sent a turn, but it is not a status and never enters a rollup.
12
+ */
13
+ export type SessionState = 'needs' | 'running' | 'idle' | 'blank';
14
+ /** Classify one session from the host's list summary; nothing is inferred from silence.
15
+ * @param session - Session summary from `session/list`.
16
+ * @param pending - Whether this client holds an unanswered interaction for that session.
17
+ * @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
18
+ */
19
+ export declare function sessionState(session: ObjectValue, pending?: boolean): SessionState;
20
+ /** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
21
+ export declare const SESSION_MARKERS: Record<SessionState, string>;
22
+ /** One word per state, so every screen names the same state the same way. */
23
+ export declare const STATE_LABELS: Record<SessionState, string>;
24
+ /** Coarse age of a session's last activity, so the column stays steady between list refreshes.
25
+ * @param time - Epoch milliseconds of the last activity, when the summary reported one.
26
+ * @param now - Current epoch milliseconds.
27
+ * @returns `now`, minutes, hours or days.
28
+ */
29
+ export declare function activityAge(time: number | undefined, now: number): string;
30
+ /** Status cell for one session row: its state marker and the age of its last activity.
31
+ * @param session - Session summary from `session/list`.
32
+ * @param now - Current epoch milliseconds.
33
+ * @param pending - Whether this client holds an unanswered interaction for that session.
34
+ * @returns Marker with an optional age, without a trailing space when unknown.
35
+ */
36
+ export declare function sessionStatus(session: ObjectValue, now: number, pending?: boolean): string;
37
+ /** States a workspace rollup reports, most actionable first. */
38
+ export declare const ROLLUP_STATES: readonly ["needs", "running", "idle"];
39
+ /** One state a workspace rollup reports. */
40
+ export type RollupState = (typeof ROLLUP_STATES)[number];
41
+ /** One counted state of a workspace rollup. */
42
+ export interface RollupCount {
43
+ state: RollupState;
44
+ count: number;
45
+ }
46
+ /** How much room a rollup has for words. */
47
+ export type RollupStyle = 'words' | 'badges';
48
+ /** Count the sessions of one workspace by the state each reports.
49
+ *
50
+ * Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
51
+ * absence of activity, and listing it beside real work only makes the rollup harder to read.
52
+ * @param sessions - Sessions whose `sessionIds` belong to the workspace.
53
+ * @param pending - Session IDs this client holds an unanswered interaction for.
54
+ * @returns One count per state that occurs, most actionable first, or an empty list.
55
+ */
56
+ export declare function workspaceCounts(sessions: readonly ObjectValue[], pending?: ReadonlySet<string>): RollupCount[];
57
+ /** Render one rollup as separately coloured cells.
58
+ *
59
+ * Each cell after the first carries the separator that joins it to the previous one, so a caller can
60
+ * colour the cells independently without losing the text {@link workspaceStatus} would produce.
61
+ * @param counts - Counts from {@link workspaceCounts}.
62
+ * @param style - `words` spells each state out; `badges` keeps only the marker and the count.
63
+ * @returns The cells in the order given, with their separators.
64
+ */
65
+ export declare function workspaceSegments(counts: readonly RollupCount[], style?: RollupStyle): {
66
+ state: RollupState;
67
+ text: string;
68
+ }[];
69
+ /** Render one rollup as plain text, the same way every screen and test reads it.
70
+ * @param counts - Counts from {@link workspaceCounts}.
71
+ * @param style - `words` spells each state out; `badges` keeps only the marker and the count.
72
+ * @returns The joined cell text, empty when nothing was counted.
73
+ */
74
+ export declare function workspaceStatus(counts: readonly RollupCount[], style?: RollupStyle): string;
75
+ /** Marker key for the compact rollup, which has no room for the words. */
76
+ export declare const ROLLUP_LEGEND: string;
77
+ /** Secondary path text for one workspace row.
78
+ *
79
+ * The title is usually the last path segment, so repeating it wastes the row; the parent directory
80
+ * is what distinguishes two checkouts. A title that does not name the last segment keeps the full
81
+ * path, because dropping it would hide where the workspace actually lives.
82
+ * @param path - Registered host directory.
83
+ * @param title - Workspace title as the row already shows it.
84
+ * @returns The path to show beside the row, or an empty string when nothing is left.
85
+ */
86
+ export declare function workspaceDetail(path: string, title: string): string;
@@ -0,0 +1,107 @@
1
+ /** Classify one session from the host's list summary; nothing is inferred from silence.
2
+ * @param session - Session summary from `session/list`.
3
+ * @param pending - Whether this client holds an unanswered interaction for that session.
4
+ * @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
5
+ */
6
+ export function sessionState(session, pending = false) {
7
+ if (pending)
8
+ return 'needs';
9
+ if (session.running === true)
10
+ return 'running';
11
+ return session.blank === true ? 'blank' : 'idle';
12
+ }
13
+ /** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
14
+ export const SESSION_MARKERS = { needs: '?', running: '◐', idle: '●', blank: '○' };
15
+ /** One word per state, so every screen names the same state the same way. */
16
+ export const STATE_LABELS = { needs: 'needs you', running: 'working', idle: 'ready', blank: 'empty' };
17
+ /** Coarse age of a session's last activity, so the column stays steady between list refreshes.
18
+ * @param time - Epoch milliseconds of the last activity, when the summary reported one.
19
+ * @param now - Current epoch milliseconds.
20
+ * @returns `now`, minutes, hours or days.
21
+ */
22
+ export function activityAge(time, now) {
23
+ if (time === undefined || !Number.isFinite(time))
24
+ return '';
25
+ const seconds = Math.max(0, Math.floor((now - time) / 1000));
26
+ if (seconds < 60)
27
+ return 'now';
28
+ const minutes = Math.floor(seconds / 60);
29
+ if (minutes < 60)
30
+ return `${minutes}m`;
31
+ const hours = Math.floor(minutes / 60);
32
+ return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
33
+ }
34
+ /** Status cell for one session row: its state marker and the age of its last activity.
35
+ * @param session - Session summary from `session/list`.
36
+ * @param now - Current epoch milliseconds.
37
+ * @param pending - Whether this client holds an unanswered interaction for that session.
38
+ * @returns Marker with an optional age, without a trailing space when unknown.
39
+ */
40
+ export function sessionStatus(session, now, pending = false) {
41
+ const age = activityAge(typeof session.updatedAt === 'number' ? session.updatedAt : undefined, now);
42
+ return `${SESSION_MARKERS[sessionState(session, pending)]}${age === '' ? '' : ` ${age}`}`;
43
+ }
44
+ /** States a workspace rollup reports, most actionable first. */
45
+ export const ROLLUP_STATES = ['needs', 'running', 'idle'];
46
+ /** Count the sessions of one workspace by the state each reports.
47
+ *
48
+ * Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
49
+ * absence of activity, and listing it beside real work only makes the rollup harder to read.
50
+ * @param sessions - Sessions whose `sessionIds` belong to the workspace.
51
+ * @param pending - Session IDs this client holds an unanswered interaction for.
52
+ * @returns One count per state that occurs, most actionable first, or an empty list.
53
+ */
54
+ export function workspaceCounts(sessions, pending = new Set()) {
55
+ const counts = { needs: 0, running: 0, idle: 0 };
56
+ for (const session of sessions) {
57
+ const id = session.sessionId;
58
+ const state = sessionState(session, typeof id === 'string' && pending.has(id));
59
+ if (state !== 'blank')
60
+ counts[state] += 1;
61
+ }
62
+ return ROLLUP_STATES.filter(state => counts[state] > 0).map(state => ({ state, count: counts[state] }));
63
+ }
64
+ /** Render one rollup as separately coloured cells.
65
+ *
66
+ * Each cell after the first carries the separator that joins it to the previous one, so a caller can
67
+ * colour the cells independently without losing the text {@link workspaceStatus} would produce.
68
+ * @param counts - Counts from {@link workspaceCounts}.
69
+ * @param style - `words` spells each state out; `badges` keeps only the marker and the count.
70
+ * @returns The cells in the order given, with their separators.
71
+ */
72
+ export function workspaceSegments(counts, style = 'words') {
73
+ const separator = style === 'words' ? ' · ' : ' ';
74
+ return counts.map(({ state, count }, index) => ({ state,
75
+ text: `${index === 0 ? '' : separator}${style === 'words' ? `${SESSION_MARKERS[state]} ${count} ${STATE_LABELS[state]}` : `${SESSION_MARKERS[state]}${count}`}` }));
76
+ }
77
+ /** Render one rollup as plain text, the same way every screen and test reads it.
78
+ * @param counts - Counts from {@link workspaceCounts}.
79
+ * @param style - `words` spells each state out; `badges` keeps only the marker and the count.
80
+ * @returns The joined cell text, empty when nothing was counted.
81
+ */
82
+ export function workspaceStatus(counts, style = 'words') {
83
+ return workspaceSegments(counts, style).map(segment => segment.text).join('');
84
+ }
85
+ /** Marker key for the compact rollup, which has no room for the words. */
86
+ export const ROLLUP_LEGEND = `${SESSION_MARKERS.idle} ${STATE_LABELS.idle} · ${SESSION_MARKERS.running} ${STATE_LABELS.running} · ${SESSION_MARKERS.needs} ${STATE_LABELS.needs}`;
87
+ /** Secondary path text for one workspace row.
88
+ *
89
+ * The title is usually the last path segment, so repeating it wastes the row; the parent directory
90
+ * is what distinguishes two checkouts. A title that does not name the last segment keeps the full
91
+ * path, because dropping it would hide where the workspace actually lives.
92
+ * @param path - Registered host directory.
93
+ * @param title - Workspace title as the row already shows it.
94
+ * @returns The path to show beside the row, or an empty string when nothing is left.
95
+ */
96
+ export function workspaceDetail(path, title) {
97
+ if (!path)
98
+ return '';
99
+ const trimmed = path.replace(/\/+$/u, '');
100
+ const cut = trimmed.lastIndexOf('/');
101
+ const base = cut < 0 ? trimmed : trimmed.slice(cut + 1);
102
+ if (base !== title)
103
+ return path;
104
+ if (cut > 0)
105
+ return trimmed.slice(0, cut);
106
+ return cut === 0 ? '/' : '';
107
+ }
@@ -0,0 +1,47 @@
1
+ import type { HistoryRow, RowKind } from '../../contracts.ts';
2
+ import type { ShellBlock } from '../../contracts.ts';
3
+ /** The part of a transcript layout this module needs to place a block. */
4
+ export interface RowSource {
5
+ /** Number of host rows, live tail included. */
6
+ length: number;
7
+ /** Host rows in a range. */
8
+ viewport(start: number, end: number): HistoryRow[];
9
+ /** Projected messages in ascending sequence order. */
10
+ messages: readonly {
11
+ seq: number;
12
+ }[];
13
+ /** Row index where each visible message starts. */
14
+ offsets: ReadonlyMap<number, number>;
15
+ }
16
+ /** Rows for one run: the command bar, then its wrapped and indented output.
17
+ * @param run - Block from the shell controller.
18
+ * @param width - Available terminal columns.
19
+ * @returns Rows in display order.
20
+ */
21
+ export declare function blockRows(run: ShellBlock, width: number): HistoryRow[];
22
+ /** Splice every retained block into a transcript layout.
23
+ *
24
+ * Equal anchors keep creation order, and an anchor older than a block already placed is clamped to
25
+ * it, so the merged stream stays ordered even as history is paged in behind the reader.
26
+ * @param layout - Transcript layout to merge with.
27
+ * @param runs - Blocks from the shell controller, oldest first.
28
+ * @param width - Available terminal columns.
29
+ * @returns The merged row count and a reader over a merged range.
30
+ */
31
+ export declare function mergeShellRuns(layout: RowSource, runs: readonly ShellBlock[], width: number): {
32
+ total: number;
33
+ /** Merged row index of each block bar that opens a readable source, to its source id. */
34
+ sources: ReadonlyMap<number, string>;
35
+ viewport(start: number, end: number): HistoryRow[];
36
+ };
37
+ /** Wrap plain local text into terminal rows, preserving its own whitespace.
38
+ *
39
+ * Command output is aligned by spaces and indented by stack traces, so this never collapses runs of
40
+ * whitespace the way tool summaries do; only the terminal width decides where a row breaks.
41
+ * @param text - Raw text, possibly containing newlines.
42
+ * @param width - Available terminal columns.
43
+ * @param kind - Row kind used for coloring.
44
+ * @param highlight - Whether the rows are a local command line drawn on the command bar.
45
+ * @returns One row per wrapped terminal line; empty text yields one empty row.
46
+ */
47
+ export declare function plainRows(text: string, width: number, kind: RowKind, highlight?: boolean): HistoryRow[];
@@ -0,0 +1,145 @@
1
+ /** Local `!` blocks as transcript rows: a highlighted command bar, then its indented output.
2
+ *
3
+ * A block is spliced into the conversation where it happened — after the newest record the session
4
+ * had when the command started — so it scrolls away like any message instead of sitting pinned to
5
+ * the bottom of the screen.
6
+ */
7
+ import stringWidth from 'string-width';
8
+ import wrapAnsi from 'wrap-ansi';
9
+ /** First output row's marker, so a block reads as the command's result. */
10
+ const MARKER = ' ⎿ ';
11
+ /** Rows after the first, aligned under the marker. */
12
+ const GUTTER = ' ';
13
+ /** Rows for one run: the command bar, then its wrapped and indented output.
14
+ * @param run - Block from the shell controller.
15
+ * @param width - Available terminal columns.
16
+ * @returns Rows in display order.
17
+ */
18
+ export function blockRows(run, width) {
19
+ const reserve = Math.max(stringWidth(MARKER), stringWidth(GUTTER));
20
+ const rows = [{ text: run.kind === 'note' ? run.command : `! ${run.command}`,
21
+ kind: 'shell', bold: true, highlight: true }];
22
+ // A note produced nothing to show, so its second row is the action itself. The label is the whole
23
+ // affordance — a sentence saying "click this bar" left the reader clicking a row that was not the
24
+ // bar — and every row of a note opens the source (see `mergeShellRuns`).
25
+ if (run.kind === 'note') {
26
+ rows.push({ text: run.source === undefined ? `${MARKER}no readable session yet` : `${MARKER}view`,
27
+ kind: 'shell', bold: true });
28
+ return rows;
29
+ }
30
+ const body = [
31
+ ...(run.dropped > 0 ? [`… ${run.dropped} earlier lines dropped …`] : []),
32
+ ...run.lines,
33
+ ];
34
+ if (!body.length)
35
+ body.push(run.status === 'running' ? 'running…' : '(no output)');
36
+ body.forEach((line, index) => {
37
+ const wrapped = plainRows(line, Math.max(8, width - reserve), 'shell');
38
+ wrapped.forEach((row, position) => {
39
+ rows.push({ ...row, text: `${index === 0 && position === 0 ? MARKER : GUTTER}${row.text}` });
40
+ });
41
+ });
42
+ if (run.status === 'exited' && (run.signal != null || (run.code ?? 0) !== 0)) {
43
+ rows.push({ text: `${GUTTER}exit ${run.signal ?? run.code}`, kind: 'muted' });
44
+ }
45
+ return rows;
46
+ }
47
+ /** Host row index where a block anchored at `anchor` belongs.
48
+ *
49
+ * It goes after every row of the newest message it followed, which is the start of the next message,
50
+ * or the end of the layout when nothing newer is loaded.
51
+ * @param layout - Transcript layout to place into.
52
+ * @param anchor - Durable sequence the command followed.
53
+ * @returns Host row index.
54
+ */
55
+ function rowAfter(layout, anchor) {
56
+ let low = 0, high = layout.messages.length;
57
+ while (low < high) {
58
+ const mid = (low + high) >> 1;
59
+ if (layout.messages[mid].seq <= anchor)
60
+ low = mid + 1;
61
+ else
62
+ high = mid;
63
+ }
64
+ if (low >= layout.messages.length)
65
+ return layout.length;
66
+ return layout.offsets.get(layout.messages[low].seq) ?? layout.length;
67
+ }
68
+ /** Splice every retained block into a transcript layout.
69
+ *
70
+ * Equal anchors keep creation order, and an anchor older than a block already placed is clamped to
71
+ * it, so the merged stream stays ordered even as history is paged in behind the reader.
72
+ * @param layout - Transcript layout to merge with.
73
+ * @param runs - Blocks from the shell controller, oldest first.
74
+ * @param width - Available terminal columns.
75
+ * @returns The merged row count and a reader over a merged range.
76
+ */
77
+ export function mergeShellRuns(layout, runs, width) {
78
+ const placements = [];
79
+ const sources = new Map();
80
+ let cursor = 0;
81
+ for (const run of runs) {
82
+ const at = Math.max(cursor, Math.min(layout.length, rowAfter(layout, run.anchor)));
83
+ placements.push({ at, rows: blockRows(run, width), ...(run.source === undefined ? {} : { source: run.source }),
84
+ ...(run.kind === 'note' ? { whole: true } : {}) });
85
+ cursor = at;
86
+ }
87
+ // Segments alternate host rows and block rows in merged index order.
88
+ const segments = [];
89
+ let merged = 0, host = 0;
90
+ for (const placement of placements) {
91
+ if (placement.at > host) {
92
+ segments.push({ from: merged, count: placement.at - host, host });
93
+ merged += placement.at - host;
94
+ host = placement.at;
95
+ }
96
+ // Only a `!` block's first row is its bar; the rows under it are its output, not its identity.
97
+ // A note has no output, so all of its rows open what it points at.
98
+ if (placement.source !== undefined) {
99
+ const opening = placement.whole === true ? placement.rows.length : 1;
100
+ for (let offset = 0; offset < opening; offset += 1)
101
+ sources.set(merged + offset, placement.source);
102
+ }
103
+ segments.push({ from: merged, count: placement.rows.length, rows: placement.rows });
104
+ merged += placement.rows.length;
105
+ }
106
+ if (host < layout.length)
107
+ segments.push({ from: merged, count: layout.length - host, host });
108
+ const total = merged + Math.max(0, layout.length - host);
109
+ return {
110
+ total,
111
+ sources,
112
+ viewport(start, end) {
113
+ const rows = [];
114
+ for (const segment of segments) {
115
+ const segmentStart = segment.from, segmentEnd = segment.from + segment.count;
116
+ if (segmentEnd <= start || segmentStart >= end)
117
+ continue;
118
+ const from = Math.max(0, start - segmentStart);
119
+ const to = Math.min(segment.count, end - segmentStart);
120
+ if (to <= from)
121
+ continue;
122
+ if (segment.rows)
123
+ rows.push(...segment.rows.slice(from, to));
124
+ else
125
+ rows.push(...layout.viewport(segment.host + from, segment.host + to));
126
+ }
127
+ return rows;
128
+ },
129
+ };
130
+ }
131
+ /** Wrap plain local text into terminal rows, preserving its own whitespace.
132
+ *
133
+ * Command output is aligned by spaces and indented by stack traces, so this never collapses runs of
134
+ * whitespace the way tool summaries do; only the terminal width decides where a row breaks.
135
+ * @param text - Raw text, possibly containing newlines.
136
+ * @param width - Available terminal columns.
137
+ * @param kind - Row kind used for coloring.
138
+ * @param highlight - Whether the rows are a local command line drawn on the command bar.
139
+ * @returns One row per wrapped terminal line; empty text yields one empty row.
140
+ */
141
+ export function plainRows(text, width, kind, highlight = false) {
142
+ const columns = Math.max(1, width);
143
+ const wrapped = wrapAnsi(text === '' ? ' ' : text, columns, { hard: true, trim: false });
144
+ return wrapped.split('\n').map(line => ({ text: line, kind, ...(highlight ? { highlight: true } : {}) }));
145
+ }
@@ -1,5 +1,49 @@
1
- import type { Controller } from '../../controller/controller.ts';
2
- import { type ObjectValue } from '../../transport/wire.ts';
1
+ import type { ClientActivity, Coverage, LivePhase } from '../../contracts.ts';
2
+ /** One formatted cost scope, with the raw totals the money column needs. */
3
+ export interface StatusCostLine {
4
+ text: string;
5
+ amount: number;
6
+ unknown: number;
7
+ }
8
+ /** Everything the status bar renders, as plain data the composition root assembles. */
9
+ export interface StatusSource {
10
+ /** Host origin, so a reader can tell which deployment the numbers describe. */
11
+ host: string;
12
+ online: boolean;
13
+ status: string;
14
+ running: boolean;
15
+ /** What the client is working on now, merged by the controller: a host turn or a running loop.
16
+ *
17
+ * The bar renders this and never merges sources itself, so "busy", its clock and the loop's
18
+ * sub-state all come from one controller-owned answer.
19
+ */
20
+ activity?: ClientActivity;
21
+ sessionId?: string;
22
+ sessionMode?: string;
23
+ workspaceLabel: string;
24
+ activeTurnStartedAt?: number;
25
+ pendingCount: number;
26
+ /** What the live attempt is doing now, straight from the record. */
27
+ livePhase?: LivePhase;
28
+ /** Retained host projections for the selected session. */
29
+ values: ObjectValue;
30
+ queued?: number;
31
+ jobs?: number;
32
+ defaultModel?: ObjectValue;
33
+ /** Billing summary; absent when this run has no ledger. */
34
+ cost?: {
35
+ sessionText: string;
36
+ /** Read on every render, so a day rollover reaches the bar without an application re-render. */
37
+ today(): StatusCostLine;
38
+ session(): StatusCostLine | undefined;
39
+ coverage: Coverage;
40
+ error?: string;
41
+ };
42
+ controlError?: string;
43
+ presetError?: string;
44
+ modelError?: string;
45
+ }
46
+ import type { ObjectValue } from '../../json.ts';
3
47
  /** Format elapsed wall time, clamping clock skew instead of displaying negative durations.
4
48
  * @param milliseconds - Elapsed duration.
5
49
  * @returns Minute/second display, with hours when needed.
@@ -77,7 +121,7 @@ export declare function phaseText(milliseconds: number): string;
77
121
  export declare function compactStatusRows(groups: StatusGroups, width: number): StatusSegment[][];
78
122
  /** Render a live clock and selected-session metadata; the timer belongs to this mounted bar. */
79
123
  export declare const StatusBar: import("react").NamedExoticComponent<{
80
- controller: Controller;
124
+ source: StatusSource;
81
125
  expanded?: boolean;
82
126
  width?: number;
83
127
  revision?: number;