moqi-tui 0.2.0

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +782 -0
  3. package/bin/moqi.mjs +40 -0
  4. package/cordis.patch.yml +41 -0
  5. package/lib/cross-find.js +217 -0
  6. package/lib/file-index.js +121 -0
  7. package/lib/fleet-sources.js +114 -0
  8. package/lib/index.js +3999 -0
  9. package/lib/persist.js +194 -0
  10. package/lib/plugins.js +371 -0
  11. package/lib/presence.js +144 -0
  12. package/lib/rename.js +35 -0
  13. package/lib/rewind.js +94 -0
  14. package/lib/sessions-store.js +134 -0
  15. package/lib/startup.js +92 -0
  16. package/lib/tui/atfile.js +154 -0
  17. package/lib/tui/export.js +48 -0
  18. package/lib/tui/fleet.js +346 -0
  19. package/lib/tui/i18n.js +201 -0
  20. package/lib/tui/jobs.js +65 -0
  21. package/lib/tui/keys.js +205 -0
  22. package/lib/tui/markdown.js +368 -0
  23. package/lib/tui/mcp.js +95 -0
  24. package/lib/tui/panels.js +231 -0
  25. package/lib/tui/screen.js +156 -0
  26. package/lib/tui/state.js +502 -0
  27. package/lib/tui/stream.js +109 -0
  28. package/lib/tui/text.js +173 -0
  29. package/lib/tui/theme.js +183 -0
  30. package/lib/tui/themes.js +153 -0
  31. package/lib/tui/tooldetail.js +140 -0
  32. package/lib/tui/view.js +830 -0
  33. package/lib/tui/vim.js +222 -0
  34. package/lib/tui-host-core.js +141 -0
  35. package/lib/tui-host.js +48 -0
  36. package/lib/types/cross-find.d.ts +66 -0
  37. package/lib/types/file-index.d.ts +34 -0
  38. package/lib/types/fleet-sources.d.ts +34 -0
  39. package/lib/types/index.d.ts +51 -0
  40. package/lib/types/persist.d.ts +116 -0
  41. package/lib/types/plugins.d.ts +218 -0
  42. package/lib/types/presence.d.ts +48 -0
  43. package/lib/types/rename.d.ts +32 -0
  44. package/lib/types/rewind.d.ts +75 -0
  45. package/lib/types/sessions-store.d.ts +46 -0
  46. package/lib/types/startup.d.ts +45 -0
  47. package/lib/types/tui/atfile.d.ts +90 -0
  48. package/lib/types/tui/export.d.ts +18 -0
  49. package/lib/types/tui/fleet.d.ts +209 -0
  50. package/lib/types/tui/i18n.d.ts +34 -0
  51. package/lib/types/tui/jobs.d.ts +28 -0
  52. package/lib/types/tui/keys.d.ts +52 -0
  53. package/lib/types/tui/markdown.d.ts +14 -0
  54. package/lib/types/tui/mcp.d.ts +34 -0
  55. package/lib/types/tui/panels.d.ts +125 -0
  56. package/lib/types/tui/screen.d.ts +79 -0
  57. package/lib/types/tui/state.d.ts +323 -0
  58. package/lib/types/tui/stream.d.ts +78 -0
  59. package/lib/types/tui/text.d.ts +28 -0
  60. package/lib/types/tui/theme.d.ts +87 -0
  61. package/lib/types/tui/themes.d.ts +70 -0
  62. package/lib/types/tui/tooldetail.d.ts +45 -0
  63. package/lib/types/tui/view.d.ts +163 -0
  64. package/lib/types/tui/vim.d.ts +64 -0
  65. package/lib/types/tui-host-core.d.ts +62 -0
  66. package/lib/types/tui-host.d.ts +42 -0
  67. package/lib/types/version.d.ts +8 -0
  68. package/lib/types/voice.d.ts +227 -0
  69. package/lib/version.js +32 -0
  70. package/lib/voice.js +405 -0
  71. package/package.json +119 -0
  72. package/scripts/harness-root.mjs +88 -0
  73. package/scripts/install-profile.mjs +133 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * App state: the transcript, the composer, the slash palette, and the picker.
3
+ *
4
+ * This module is deliberately free of terminal and Harness concerns so the
5
+ * interaction rules stay testable on their own — it only knows strings,
6
+ * cursors, and selections.
7
+ * @module
8
+ */
9
+ /** Minimum and maximum composer height, in text rows. */
10
+ export declare const MIN_INPUT_LINES = 1;
11
+ export declare const MAX_INPUT_LINES = 10;
12
+ /** How many sent prompts the composer history retains. */
13
+ export declare const HISTORY_LIMIT = 500;
14
+ /**
15
+ * One piece of a turn, in the order it actually happened.
16
+ *
17
+ * A turn is not prose with tool calls bolted on the side: the agent says
18
+ * something, runs a tool, says something about what came back. Holding the text
19
+ * as one string and the calls as a separate list threw that order away, so the
20
+ * prose arrived as one run of concatenated fragments under a block of calls —
21
+ * which is exactly how a turn stops making sense to read.
22
+ */
23
+ export type Segment = {
24
+ kind: 'text';
25
+ text: string;
26
+ } | {
27
+ kind: 'tool';
28
+ tool: ToolActivity;
29
+ };
30
+ /** One turn of the transcript. */
31
+ export interface Message {
32
+ role: 'user' | 'assistant';
33
+ /** The turn's pieces in arrival order — prose and tool calls interleaved. */
34
+ segments: readonly Segment[];
35
+ /** Reasoning text, shown only when thinking is toggled on. */
36
+ reasoning?: string;
37
+ /** Set when this message is a command result rather than model output. */
38
+ command?: {
39
+ name: string;
40
+ ok: boolean;
41
+ };
42
+ /** Set on a user prompt steered into a still-running turn. */
43
+ steering?: boolean;
44
+ /** Images sent with this user prompt, drawn as one summary line. */
45
+ attachments?: readonly {
46
+ name: string;
47
+ width: number;
48
+ height: number;
49
+ }[];
50
+ }
51
+ /** A turn that is nothing but text: a prompt, a command result, a log replay. */
52
+ export declare function textMessage(role: Message['role'], text: string, rest?: Omit<Message, 'role' | 'segments'>): Message;
53
+ /**
54
+ * The turn's prose with the tool calls dropped, for `/copy`, `/export`,
55
+ * `/find`, and the tab title.
56
+ *
57
+ * Segments are joined with a blank line because a tool call is where the model
58
+ * stopped and started again — running the two halves together is what made a
59
+ * reply read as one endless paragraph.
60
+ */
61
+ export declare function segmentsText(segments: readonly Segment[]): string;
62
+ export declare function messageText(message: Message): string;
63
+ /**
64
+ * Every tool row, in order. The rows are the live objects, not copies, so a
65
+ * later `tool/result` event settles the row where it already sits in the turn.
66
+ */
67
+ export declare function segmentTools(segments: readonly Segment[]): ToolActivity[];
68
+ /** Every tool row in the turn, in order — for result events and counting. */
69
+ export declare function messageTools(message: Message): ToolActivity[];
70
+ /**
71
+ * Append streamed text to the turn, continuing the trailing run when there is
72
+ * one. A tool call in between is what opens a new text segment, which is how
73
+ * the order gets recorded at all.
74
+ */
75
+ export declare function appendText(segments: Segment[], text: string): void;
76
+ /** The tool row for a call id, wherever it sits in the turn. */
77
+ export declare function findTool(segments: readonly Segment[], match: (tool: ToolActivity) => boolean): ToolActivity | undefined;
78
+ /** A single tool invocation surfaced in the transcript. */
79
+ export interface ToolActivity {
80
+ /** Provider-issued call id; the only stable handle across argument deltas. */
81
+ id?: string;
82
+ name: string;
83
+ status: 'running' | 'ok' | 'error';
84
+ detail?: string;
85
+ /** One line of what came back, rendered under the call it belongs to. */
86
+ result?: string;
87
+ }
88
+ /**
89
+ * Whether a turn's queued prompts should send once it settles.
90
+ *
91
+ * A clean finish always drains the queue. An interrupt is the user asking for
92
+ * silence — so it freezes the queue — *unless* the interrupt was asked for as
93
+ * a redirection (`/interrupt`): stop this answer, then run what was queued.
94
+ */
95
+ export declare function queueShouldDrain(options: {
96
+ interrupted: boolean;
97
+ drainRequested: boolean;
98
+ }): boolean;
99
+ /**
100
+ * What a session is doing, for the tab bar.
101
+ *
102
+ * `ready` is the state worth interrupting someone for: the turn finished and
103
+ * the answer has not been seen. It is what the bell announces.
104
+ */
105
+ export type SessionStatus = 'idle' | 'running' | 'ready';
106
+ /** One open session, as the tab bar shows it. */
107
+ export interface SessionSummary {
108
+ id: string;
109
+ title: string;
110
+ status: SessionStatus;
111
+ active: boolean;
112
+ }
113
+ /**
114
+ * A live agent other than the one the transcript is showing.
115
+ *
116
+ * The Harness can run delegated work — subagents the foreground turn spawned —
117
+ * and without a row here that work is invisible: the screen looks idle while
118
+ * the machine is busy.
119
+ */
120
+ export interface BackgroundAgent {
121
+ /** Session id, used as the stable identity. */
122
+ id: string;
123
+ /** Short human label: the preset name when composed from one. */
124
+ label: string;
125
+ status: 'idle' | 'running';
126
+ /** Delegation depth; 1 is a direct child of the foreground agent. */
127
+ depth: number;
128
+ /** Epoch millis when this agent was first seen. */
129
+ startedAt: number;
130
+ }
131
+ /** A slash command as the palette shows it. */
132
+ export interface PaletteCommand {
133
+ name: string;
134
+ args: string;
135
+ description: string;
136
+ }
137
+ /** Which list the picker is currently showing. */
138
+ export type PickerKind = 'sessions' | 'models' | 'themes' | 'plugins' | 'open' | 'delete' | 'rewind' | 'stored' | 'lang' | 'none';
139
+ /** One row in the picker. */
140
+ export interface PickerItem {
141
+ id: string;
142
+ title: string;
143
+ subtitle: string;
144
+ /** Set on a model row: the provider route that owns the model. */
145
+ provider?: string;
146
+ /** Set on a model row: the model id passed to the request. */
147
+ model?: string;
148
+ /** Marks the row that is currently in use. */
149
+ active?: boolean;
150
+ }
151
+ /**
152
+ * The composer. A plain multi-line buffer with a cursor, wide enough in
153
+ * behavior to feel like an editor: word motion, line motion, and kill-to-end.
154
+ */
155
+ export declare class Composer {
156
+ private text;
157
+ private cursor;
158
+ value(): string;
159
+ position(): number;
160
+ setValue(value: string): void;
161
+ /** Replace the whole buffer and land the cursor at an explicit offset. */
162
+ adopt(text: string, cursor: number): void;
163
+ reset(): void;
164
+ insert(chunk: string): void;
165
+ backspace(): void;
166
+ deleteForward(): void;
167
+ /** Delete from the cursor back to the start of the current word. */
168
+ deleteWord(): void;
169
+ /** Delete from the cursor to the end of the buffer. */
170
+ killToEnd(): void;
171
+ /** Delete from the start of the buffer to the cursor. */
172
+ killToStart(): void;
173
+ left(): void;
174
+ right(): void;
175
+ wordLeft(): void;
176
+ wordRight(): void;
177
+ home(): void;
178
+ end(): void;
179
+ toStart(): void;
180
+ toEnd(): void;
181
+ /** Offset of the first character of the cursor's logical line. */
182
+ lineStartIndex(): number;
183
+ /** Offset of the newline (or end of buffer) that closes the cursor's line. */
184
+ lineEndIndex(): number;
185
+ /**
186
+ * Delete a half-open range and park the cursor at its start.
187
+ *
188
+ * The bounds are clamped, so a motion that ran off either end of the buffer
189
+ * deletes what it actually covered rather than throwing.
190
+ */
191
+ deleteRange(start: number, end: number): void;
192
+ /** Move the cursor one visual row up or down within the wrapped composer. */
193
+ moveRow(delta: number, width: number): void;
194
+ /**
195
+ * Wrap the buffer to `width`, returning each visual row with the buffer
196
+ * offsets it covers. The view and the cursor both read this, so they cannot
197
+ * disagree about where a row begins.
198
+ */
199
+ layout(width: number): {
200
+ text: string;
201
+ start: number;
202
+ end: number;
203
+ }[];
204
+ /** Height in rows the composer wants at `width`, clamped to the app's bounds. */
205
+ height(width: number): number;
206
+ /**
207
+ * Whether the cursor sits on the first visual row, so `↑` would otherwise be
208
+ * a no-op — the moment input-history recall should take over.
209
+ */
210
+ atFirstRow(width: number): boolean;
211
+ /** The mirror of {@link atFirstRow} for `↓` and newer history entries. */
212
+ atLastRow(width: number): boolean;
213
+ }
214
+ /**
215
+ * Recall of previously sent prompts, the way a shell recalls its history.
216
+ *
217
+ * `recall` walks older (`-1`) or newer (`+1`) entries and returns the text to
218
+ * show, or `undefined` when there is nothing further in that direction — the
219
+ * caller then falls back to ordinary cursor motion. The draft being typed is
220
+ * remembered the first time recall leaves it, so walking back down to the end
221
+ * restores it rather than stranding the user on the last sent prompt.
222
+ */
223
+ export declare class InputHistory {
224
+ private entries;
225
+ /** Position while recalling; `-1` means the live draft, not any entry. */
226
+ private index;
227
+ private draft;
228
+ /** Record a sent prompt, ignoring empties and immediate repeats. */
229
+ add(text: string): void;
230
+ /** Adopt persisted entries (oldest first), keeping the most recent ones. */
231
+ load(entries: readonly string[]): void;
232
+ /** Every recorded prompt, oldest first, for persistence. */
233
+ snapshot(): readonly string[];
234
+ /**
235
+ * Walk the history. `draft` is what the composer holds now; it is saved the
236
+ * first time recall moves away from live typing.
237
+ */
238
+ recall(delta: number, draft: string): string | undefined;
239
+ /** Whether a recall is in flight, i.e. ↑/↓ should keep walking history. */
240
+ isRecalling(): boolean;
241
+ /** Return to live typing; called whenever the composer is edited or sent. */
242
+ reset(): void;
243
+ }
244
+ /** The popup that filters slash commands as they are typed. */
245
+ export declare class Palette {
246
+ open: boolean;
247
+ matches: PaletteCommand[];
248
+ selected: number;
249
+ /**
250
+ * Recompute from the composer text. The palette lives only while the input
251
+ * is a single unfinished `/word`; once a space is typed the user has moved
252
+ * on to the command's own arguments.
253
+ */
254
+ update(input: string, commands: readonly PaletteCommand[]): void;
255
+ move(delta: number): void;
256
+ current(): PaletteCommand | undefined;
257
+ close(): void;
258
+ }
259
+ /**
260
+ * Subsequence match, the same shape of filter an editor's command palette
261
+ * uses: every character of the query appears in order, not necessarily
262
+ * adjacent, so "g53" finds "glm-5.3".
263
+ */
264
+ export declare function fuzzyMatch(query: string, text: string): boolean;
265
+ /**
266
+ * The full-pane list that replaces the transcript for `/resume` and `/model`.
267
+ *
268
+ * It filters as you type and, when grouped, prints a header each time the
269
+ * subtitle changes — so a model list reads provider by provider rather than as
270
+ * one undifferentiated column.
271
+ */
272
+ export declare class Picker {
273
+ kind: PickerKind;
274
+ title: string;
275
+ items: PickerItem[];
276
+ selected: number;
277
+ query: string;
278
+ /** Whether rows are grouped under their subtitle. */
279
+ grouped: boolean;
280
+ show(kind: Exclude<PickerKind, 'none'>, title: string, items: PickerItem[], options?: {
281
+ grouped?: boolean;
282
+ }): void;
283
+ hide(): void;
284
+ /** Rows surviving the current query, in their original order. */
285
+ matches(): PickerItem[];
286
+ /** Narrow or widen the filter, keeping the selection in range. */
287
+ setQuery(query: string): void;
288
+ move(delta: number): void;
289
+ /** Put the cursor on a given row of the unfiltered list, if it survives. */
290
+ selectById(id: string): void;
291
+ current(): PickerItem | undefined;
292
+ }
293
+ /** The minimum a session has to expose for {@link ownerOfDelegated}. */
294
+ export interface DelegationHost {
295
+ id: string;
296
+ streaming: boolean;
297
+ }
298
+ /**
299
+ * Which open session a delegated agent belongs to, as an index.
300
+ *
301
+ * The Harness does not report a delegation parent, so this is a rule rather
302
+ * than a lookup, and it is worth stating plainly because the alternative --
303
+ * one list shared by every session -- is what made another conversation's
304
+ * subagents appear in whichever tab was on screen.
305
+ *
306
+ * Fork lineage wins when it names a session that is actually open. Otherwise
307
+ * timing decides: delegated work is spawned while its parent's turn runs, so
308
+ * the streaming session claims it. With neither, the active session is the
309
+ * only honest guess.
310
+ */
311
+ export declare function ownerOfDelegated(sessions: readonly DelegationHost[], parentSessionId: string | undefined, activeIndex: number): number;
312
+ /**
313
+ * Walk a transcript selection.
314
+ *
315
+ * There is no selection until the first key: `alt+↑`/`alt+↓` then start at the
316
+ * newest turn and move, clamped at both ends and empty when there is nothing
317
+ * to select.
318
+ */
319
+ export declare function moveSelection(current: number | undefined, delta: number, count: number): number | undefined;
320
+ /** Format a token count the way a status bar wants it: 834, 1.2K, 64K. */
321
+ export declare function formatTokens(count: number): string;
322
+ /** A deliberately rough chars/4 estimate, used until real usage is reported. */
323
+ export declare function estimateTokens(text: string): number;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Pure projection of a Harness assistant-stream chunk onto the transcript.
3
+ *
4
+ * `TuiApp.onFrame` used to hold this switch inline, which made the one path
5
+ * that turns a live model reply into visible text untestable without a full
6
+ * Harness runtime. This module keeps the identical logic but depends on
7
+ * nothing: it imports no Harness types and no npm packages, so a synthetic
8
+ * chunk sequence can be replayed in a dependency-free test (see
9
+ * `tests/stream-smoke.ts`) the same way `render-smoke.ts` exercises `view.ts`.
10
+ *
11
+ * @module moqi-tui/tui/stream
12
+ */
13
+ import type { Segment } from './state.ts';
14
+ /**
15
+ * The mutable streaming surface a chunk projects onto — a structural subset of
16
+ * the app's session tab, so the function works on the real tab or a test stub
17
+ * with no Harness types in sight.
18
+ */
19
+ export interface StreamingSurface {
20
+ /** The turn so far, prose and calls in the order they arrived. */
21
+ streamingSegments: Segment[];
22
+ streamingReasoning: string;
23
+ promptTokens: number;
24
+ completionTokens: number;
25
+ totalTokens: number;
26
+ haveUsage: boolean;
27
+ /** Prompt tokens served from the provider's cache, when it reports them. */
28
+ cacheReadTokens: number;
29
+ /** Prompt tokens written to the provider's cache, when it reports them. */
30
+ cacheWriteTokens: number;
31
+ /** Output tokens per second for the last settled turn, when measurable. */
32
+ tps: number;
33
+ }
34
+ /**
35
+ * Structural view of a Harness `StreamChunk`, carrying only the fields the TUI
36
+ * renders. It is deliberately wide (every field optional) so the real union —
37
+ * whose variants carry `index`, `argumentsDelta`, `reason`, etc. — assigns to
38
+ * it without a cast, and unknown future chunk kinds fall through the default.
39
+ */
40
+ export interface StreamChunkLike {
41
+ type: string;
42
+ text?: string;
43
+ id?: string | number;
44
+ name?: string;
45
+ usage?: {
46
+ inputTokens: number;
47
+ outputTokens: number;
48
+ totalTokens?: number;
49
+ cacheReadTokens?: number;
50
+ cacheWriteTokens?: number;
51
+ };
52
+ block?: {
53
+ type: string;
54
+ id?: string | number;
55
+ name?: string;
56
+ arguments?: string;
57
+ };
58
+ /** Rendered fields the union carries but this app ignores. */
59
+ index?: number;
60
+ argumentsDelta?: string;
61
+ blockType?: unknown;
62
+ reason?: unknown;
63
+ replayState?: unknown;
64
+ }
65
+ /**
66
+ * Apply one stream chunk to a streaming surface, mutating it in place.
67
+ *
68
+ * `text-delta` continues the turn's trailing run of prose, a `tool-call-delta`
69
+ * adds or names a running tool row keyed by call id, `usage` lands the token
70
+ * counters, and `block-end` settles the matching row to `ok`. Unknown chunk
71
+ * kinds are ignored on purpose — the chunk union is merge-extensible and a
72
+ * plugin may emit one this app has never heard of.
73
+ *
74
+ * Order is the point: text after a call opens a new segment rather than
75
+ * extending the text before it, so "checking…", the call, and "found it" stay
76
+ * three things in sequence instead of one paragraph and a detached list.
77
+ */
78
+ export declare function projectStreamChunk(surface: StreamingSurface, chunk: StreamChunkLike): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Width-aware text helpers.
3
+ *
4
+ * Every measurement in the app goes through {@link displayWidth}, which ignores
5
+ * SGR escapes and counts East Asian wide characters as two columns. Without
6
+ * that, styled or CJK content silently breaks the column arithmetic the whole
7
+ * layout depends on.
8
+ * @module
9
+ */
10
+ /** Remove every escape sequence, leaving the printable text. */
11
+ export declare function stripAnsi(text: string): string;
12
+ /** Printable width of a string in terminal columns, ignoring escapes. */
13
+ export declare function displayWidth(text: string): number;
14
+ /**
15
+ * Cut a string to `limit` columns, appending an ellipsis when it did not fit.
16
+ * Escape sequences pass through so styling is never severed mid-code, and a
17
+ * reset is appended when the cut text carried any styling.
18
+ */
19
+ export declare function truncate(text: string, limit: number): string;
20
+ /** Pad a string on the right to `width` columns. */
21
+ export declare function padEnd(text: string, width: number): string;
22
+ /**
23
+ * Hard-wrap plain text to `width` columns, breaking on spaces where possible
24
+ * and mid-word only when a single word cannot fit on a line of its own.
25
+ */
26
+ export declare function wrap(text: string, width: number): string[];
27
+ /** Take exactly as many code points as fit in `width` columns, no ellipsis. */
28
+ export declare function cut(text: string, width: number): string;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Palette and text styling for the terminal app.
3
+ *
4
+ * The colors default to the Rose Pine-ish pair the original Go client used,
5
+ * kept as explicit light/dark variants so the app reads on either terminal
6
+ * background. Everything emits truecolor SGR directly: the app already owns
7
+ * the screen, so there is no styling library between it and the escape codes.
8
+ *
9
+ * Which palette is in force is swappable at runtime — see {@link applyTheme}
10
+ * and the table in `themes.ts`. That is deliberately orthogonal to the
11
+ * light/dark question: a theme supplies both variants, and the terminal's own
12
+ * background still decides which of the two is drawn.
13
+ * @module
14
+ */
15
+ import { type Theme } from './themes.ts';
16
+ /** One color with a variant per terminal background. */
17
+ export interface AdaptiveColor {
18
+ light: string;
19
+ dark: string;
20
+ }
21
+ export declare const colAccent: AdaptiveColor;
22
+ export declare const colMuted: AdaptiveColor;
23
+ export declare const colBorder: AdaptiveColor;
24
+ export declare const colText: AdaptiveColor;
25
+ export declare const colWarn: AdaptiveColor;
26
+ export declare const colOK: AdaptiveColor;
27
+ export declare const colGreen: AdaptiveColor;
28
+ export declare const colGold: AdaptiveColor;
29
+ export declare const colRose: AdaptiveColor;
30
+ export declare const colInvert: AdaptiveColor;
31
+ /**
32
+ * Install a named palette, returning false when there is no such theme.
33
+ *
34
+ * Every other module imported the color constants by name, so the switch has
35
+ * to happen *through* those objects rather than by replacing them: an import
36
+ * binding points at the object that existed when the module was evaluated,
37
+ * and reassigning the constant here would leave every call site drawing with
38
+ * the old palette. Writing the two fields in place is what makes a theme
39
+ * change a one-line operation instead of a rewrite of every view.
40
+ *
41
+ * An unknown name is reported rather than thrown: it arrives from `/theme
42
+ * <name>` or from a state file written by a future version, and neither is a
43
+ * reason to take the app down.
44
+ */
45
+ export declare function applyTheme(name: string): boolean;
46
+ /** The name of the palette currently installed. */
47
+ export declare function activeTheme(): string;
48
+ /** Every palette {@link applyTheme} will accept, in the order to list them. */
49
+ export declare function listThemes(): readonly Theme[];
50
+ /**
51
+ * Re-exported so a caller that only wants to list or name a palette imports
52
+ * this module alone, the way every drawing module already does.
53
+ */
54
+ export type { Theme, ThemePalette } from './themes.ts';
55
+ /**
56
+ * Re-read the environment, so a change of terminal background applies without
57
+ * a restart. This is about the light/dark variant only — the choice of
58
+ * palette is {@link applyTheme}'s.
59
+ */
60
+ export declare function refreshTheme(): void;
61
+ /** Whether styling currently targets a dark background. */
62
+ export declare function isDark(): boolean;
63
+ /** Resolve an adaptive color against the active background. */
64
+ export declare function resolve(color: AdaptiveColor): string;
65
+ /** Clears every attribute set by {@link style}. */
66
+ export declare const RESET = "\u001B[0m";
67
+ /** Attributes a style can carry beyond its colors. */
68
+ export interface StyleOptions {
69
+ fg?: AdaptiveColor;
70
+ bg?: AdaptiveColor;
71
+ bold?: boolean;
72
+ italic?: boolean;
73
+ underline?: boolean;
74
+ dim?: boolean;
75
+ strike?: boolean;
76
+ }
77
+ /**
78
+ * Wrap text in a style. Each line is styled independently so a styled block
79
+ * survives being split, padded, or placed beside other cells.
80
+ */
81
+ export declare function style(text: string, options: StyleOptions): string;
82
+ export declare const muted: (text: string) => string;
83
+ export declare const warn: (text: string) => string;
84
+ export declare const ok: (text: string) => string;
85
+ export declare const bold: (text: string) => string;
86
+ export declare const accent: (text: string) => string;
87
+ export declare const selected: (text: string) => string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The palettes `/theme` can choose between.
3
+ *
4
+ * This is a data module on purpose: `theme.ts` owns the colour objects every
5
+ * other module imports by name, and it would grow unreadable if five full
6
+ * palettes sat between the `style()` machinery and the SGR encoder. Keeping
7
+ * the table here means adding a palette is an edit to one list of hex pairs
8
+ * and nothing else.
9
+ *
10
+ * Every palette carries both a light and a dark variant because the variant
11
+ * is picked separately, from the terminal background — a user on a light
12
+ * terminal must get a readable Nord, not a dark one washed out.
13
+ * @module
14
+ */
15
+ import type { AdaptiveColor } from './theme.ts';
16
+ /**
17
+ * The ten colour slots a palette has to fill.
18
+ *
19
+ * The names are the semantic roles the app draws with rather than hues, so a
20
+ * palette that has no literal gold still has to answer the question "what do
21
+ * inline code spans look like here".
22
+ */
23
+ export interface ThemePalette {
24
+ /** The app's signature colour: prompts, selections, the composer border. */
25
+ accent: AdaptiveColor;
26
+ /** Secondary text that should recede: hints, timestamps, footers. */
27
+ muted: AdaptiveColor;
28
+ /** Box rules and separators. */
29
+ border: AdaptiveColor;
30
+ /** Ordinary foreground text. */
31
+ text: AdaptiveColor;
32
+ /** Errors and anything the user should not miss. */
33
+ warn: AdaptiveColor;
34
+ /** Success: a finished tool call, a healthy device. */
35
+ ok: AdaptiveColor;
36
+ /** Activity in progress: spinners, live rows. */
37
+ green: AdaptiveColor;
38
+ /** Inline code and emphasis inside markdown. */
39
+ gold: AdaptiveColor;
40
+ /** List markers and ordinals. */
41
+ rose: AdaptiveColor;
42
+ /** Foreground against an accent background, i.e. the terminal's own base. */
43
+ invert: AdaptiveColor;
44
+ }
45
+ /** A named palette, as `/theme` lists it. */
46
+ export interface Theme {
47
+ /** The id typed after `/theme`; lowercase and hyphenated. */
48
+ name: string;
49
+ /** One line explaining what the palette is, for the picker's right column. */
50
+ description: string;
51
+ colors: ThemePalette;
52
+ }
53
+ /** The name the app starts with when nothing has been chosen or persisted. */
54
+ export declare const DEFAULT_THEME = "rose-pine";
55
+ /**
56
+ * The colours the exported constants in `theme.ts` are seeded with.
57
+ *
58
+ * They are seeded from the table rather than written out a second time so the
59
+ * default and the `rose-pine` entry cannot drift apart: if they did, `/theme
60
+ * rose-pine` would quietly stop being a way back to how the app started.
61
+ */
62
+ export declare const DEFAULT_PALETTE: ThemePalette;
63
+ /**
64
+ * Every palette, in the order `/theme` lists them: the default first, then
65
+ * the rest alphabetically, with the accessibility option last so it reads as
66
+ * the deliberate escape hatch it is.
67
+ */
68
+ export declare const THEMES: readonly Theme[];
69
+ /** Look a palette up by name, or `undefined` when no such palette exists. */
70
+ export declare function findTheme(name: string): Theme | undefined;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What a tool call says about itself, and what came back.
3
+ *
4
+ * A tool row used to carry only the tool's name — `bash` four times over said
5
+ * nothing about what the agent was doing. The raw `arguments` JSON is already
6
+ * on the call (and its result already in the session log), so the readable
7
+ * summary is a pair of pure functions over strings: no Harness types, no I/O,
8
+ * replayable in a dependency-free suite next to the stream projection.
9
+ *
10
+ * @module moqi-tui/tui/tooldetail
11
+ */
12
+ import type { ToolActivity } from './state.ts';
13
+ /**
14
+ * Summarize what a tool call does from its raw `arguments` JSON — the exact
15
+ * string the model produced, parsed leniently. `bash` becomes
16
+ * `bash docker ps --format {{.Names}}`; a read becomes its path; anything
17
+ * unrecognized falls back to its first string-valued argument, then to the
18
+ * compact JSON, then to nothing rather than to noise.
19
+ */
20
+ export declare function describeToolCall(name: string, rawArguments: string | undefined): string;
21
+ /**
22
+ * The one-line face of a tool result: its text content folded to a single
23
+ * line, or the failure's identity when the result block says the tool erred.
24
+ * `content` is the result block's content array; each text block contributes.
25
+ *
26
+ * @returns the summary, or `undefined` when the result says nothing readable.
27
+ */
28
+ export declare function summarizeResult(content: readonly unknown[], error?: {
29
+ name?: string;
30
+ code?: string;
31
+ }): string | undefined;
32
+ /**
33
+ * Fold one session-log event onto the live tool rows of the turn in flight.
34
+ *
35
+ * `tool/call` fills a row's detail (the deltas name it, the log says what it
36
+ * does); `tool/result` settles the row — `ok` or `error` — and attaches its
37
+ * outcome underneath the very call that produced it, in flow. Rows are keyed
38
+ * by call id and never created here: only stream deltas and block ends, which
39
+ * observe the same calls, add rows, so an event from an earlier turn logged
40
+ * before the sync point finds no row and is ignored.
41
+ */
42
+ export declare function applyToolEvent(tools: ToolActivity[], event: {
43
+ type?: string;
44
+ data?: Record<string, unknown> | undefined;
45
+ }): void;