@multiclawcli/dashboard 0.3.1 → 0.4.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.
package/README.md CHANGED
@@ -3,9 +3,73 @@
3
3
  [![npm](https://img.shields.io/npm/v/@multiclawcli/dashboard)](https://www.npmjs.com/package/@multiclawcli/dashboard)
4
4
  [![GitHub](https://img.shields.io/badge/GitHub-shipengqi%2Fmulticlaw-blue?logo=github)](https://github.com/shipengqi/multiclaw)
5
5
 
6
- Real-time dashboard for [multiclaw](https://github.com/shipengqi/multiclaw) — the multi-agent orchestration CLI.
6
+ Interactive terminal console for [multiclaw](https://github.com/shipengqi/multiclaw) — the multi-agent
7
+ orchestration CLI.
7
8
 
8
- This package provides the web UI and WebSocket server used internally by the `multiclaw` CLI. It is not intended for direct use; install [`multiclaw`](https://www.npmjs.com/package/multiclaw) instead.
9
+ This package provides the Ink-based TUI used internally by the `multiclaw` CLI when it is run with
10
+ no subcommand. It is not intended for direct use; install
11
+ [`multiclaw`](https://www.npmjs.com/package/multiclaw) instead.
12
+
13
+ ## What it exports
14
+
15
+ | Export | Purpose |
16
+ |--------|---------|
17
+ | `./tui` → `startTui({ config, onTurnStart })` | Render the console and drive the turn loop |
18
+ | `./tui` → `tuiReducer`, `reduceEvent`, `initialState`, `appendLog`, `stripAnsi` | Pure state machine |
19
+ | `./tui` → `TurnView`, `TuiState`, `MAX_HISTORY` | The turn/console split that history is built on |
20
+ | `./tui` → `computeLayout` | Terminal geometry: which regions fit at a given size |
21
+
22
+ `reduceEvent` is a pure `(state, event) => state` function over core's `MultiClawEvent`, and
23
+ `computeLayout` is pure arithmetic over `(columns, rows, …)`. Both the state layer and the responsive
24
+ rules are therefore testable without a terminal.
25
+
26
+ ## One turn, and the console around it
27
+
28
+ `TuiState` is split in two, and the split is what makes history possible:
29
+
30
+ | Type | Holds |
31
+ |------|-------|
32
+ | `TurnView` | Everything that describes *a turn*: agents, stages, focus, scroll, phase, reply |
33
+ | `TuiState extends TurnView` | The above, for the live turn, plus `name`, `history` and `view` |
34
+
35
+ A finished turn is just a `TurnView` kept in `history`, and every pane takes a `TurnView` — so a past
36
+ turn renders through exactly the same components as the live one. `view` is a *detached* copy of the
37
+ turn being read: `mapShown` routes `tab`, scroll and `/clear` to `view` when one is open, so browsing
38
+ cannot move the focus out from under a run in progress, and cannot rewrite history either.
39
+
40
+ ## Source layout
41
+
42
+ | Path | Holds |
43
+ |------|-------|
44
+ | `src/tui/state.ts` | The reducer, the roster seed, the log window, the live clocks, the turn history |
45
+ | `src/tui/layout.ts` | Row budget and breakpoints |
46
+ | `src/tui/format.ts` | Duration, truncation and padding helpers |
47
+ | `src/tui/commands.ts` | The `/` command palette model |
48
+ | `src/tui/theme.ts` | Colour, glyph and spinner tokens |
49
+ | `src/tui/components/` | Ink components, grouped by role |
50
+ | `src/tui/App.tsx` | Keymap and wiring |
51
+
52
+ Coverage is scoped to the four decision-carrying modules (`state`, `layout`, `format`, `commands`).
53
+ The Ink components are render glue, so they are covered by frame assertions in `components.test.tsx`
54
+ and `App.test.tsx` rather than by line counts.
55
+
56
+ ## Notes
57
+
58
+ - The console requires a TTY. Without one, `startTui` throws and the CLI points at `multiclaw run`.
59
+ - Rendering happens on the alternate screen buffer, which is always restored on exit.
60
+ - Per-agent log buffers are capped (tail window) so a long-running agent cannot exhaust memory.
61
+ - The prompt holds focus at all times, so only `?` and `/` are reserved as bare keys, and only while
62
+ the line is empty *and* the console is idle. Everything else is a named key or an ordinary
63
+ character. Mid-run `?` therefore types a literal `?`, because that is what the user is doing.
64
+ - The prompt stays live while a turn runs. Submitting queues the text, and the queue starts as the
65
+ next turn once the console is idle — `App.tsx` holds it in a small array, `QueueStrip` renders it,
66
+ and `computeLayout` charges its rows to the body.
67
+ - Every turn runs against the same `workDir`. Only the logs are per-run
68
+ (`.multiclaw/runs/run-<stamp>/logs/`), which is what lets a follow-up build on the last turn.
69
+ - The hint bar only lists keys that do something *right now*. A turn that ended in a reply takes the
70
+ body, so there is no roster to switch between and `tab` is left out rather than promised.
71
+ - The package name is historical: it previously shipped a React web dashboard. Renaming it would
72
+ invalidate the npm trusted-publisher configuration, so the name stays.
9
73
 
10
74
  ## Documentation
11
75
 
package/dist/tui.d.ts ADDED
@@ -0,0 +1,299 @@
1
+ import { MultiClawConfig, EventBus, AgentStatus, MultiClawEvent, StageInfo, LeaderMessage, TurnRecord, AgentDefinition } from '@multiclawcli/core';
2
+
3
+ interface AppProps {
4
+ config: MultiClawConfig;
5
+ /**
6
+ * Called once per turn, after that turn's orchestrator exists. The CLI uses it
7
+ * to attach a file log writer without the TUI depending on the CLI package.
8
+ */
9
+ onTurnStart?: (eventBus: EventBus, logDir: string) => void;
10
+ }
11
+
12
+ /**
13
+ * Terminal geometry.
14
+ *
15
+ * Kept pure and separate from the components because responsive rules are the
16
+ * part of a TUI that is easiest to get subtly wrong and hardest to eyeball. Every
17
+ * row the console spends is budgeted here, so the arithmetic is testable rather
18
+ * than buried in JSX.
19
+ */
20
+ declare const MIN_COLUMNS = 40;
21
+ declare const MIN_ROWS = 10;
22
+ /** Below this the roster is dropped so the stream keeps a usable width. */
23
+ declare const COMPACT_COLUMNS = 72;
24
+ /** The rail costs a row; it only earns one when there is vertical room. */
25
+ declare const RAIL_MIN_ROWS = 16;
26
+ /**
27
+ * The most rows the pipeline rail is ever allowed to occupy.
28
+ *
29
+ * The rail wraps, so its height depends on how many agents the turn has and how
30
+ * wide the terminal is. Without a cap it could eat the whole screen; with one,
31
+ * the budget stays honest and anything past it is clipped rather than silently
32
+ * swallowed by the body.
33
+ */
34
+ declare const MAX_RAIL_ROWS = 2;
35
+ /** Enough to see the palette is a list; more would push the stream off screen. */
36
+ declare const MAX_MENU_ROWS = 7;
37
+ /** Queued follow-ups sit above the prompt; more than a few would crowd the stream. */
38
+ declare const MAX_QUEUE_ROWS = 3;
39
+ interface LayoutInput {
40
+ columns: number;
41
+ rows: number;
42
+ /** Zero means no turn has run yet, which swaps the body for the welcome screen. */
43
+ turn: number;
44
+ /** A modal owns the body — help or the history list. */
45
+ overlay: boolean;
46
+ /** Rows the command palette is asking for. */
47
+ menuRows: number;
48
+ /** Rows the queued follow-ups are asking for. */
49
+ queueRows?: number;
50
+ /** Whether a command result is waiting to be shown. */
51
+ notice: boolean;
52
+ /** A past turn is open for reading, which costs a banner row. */
53
+ viewing?: boolean;
54
+ /** Whether there is a leader or any stage to draw. */
55
+ hasRail: boolean;
56
+ /**
57
+ * Rendered widths of the rail's cells, widest first is not required — order
58
+ * is what the rail draws. Needed because the rail wraps: its height is a
59
+ * function of the content, so a flat "one row" charge under-budgets it.
60
+ */
61
+ railCells?: number[];
62
+ }
63
+ interface Layout {
64
+ columns: number;
65
+ rows: number;
66
+ compact: boolean;
67
+ showTeam: boolean;
68
+ showRail: boolean;
69
+ /** How many rows the rail will actually occupy once it wraps. */
70
+ railRows: number;
71
+ welcome: boolean;
72
+ bodyHeight: number;
73
+ logViewport: number;
74
+ teamWidth: number;
75
+ menuRows: number;
76
+ queueRows: number;
77
+ }
78
+ /**
79
+ * How many rows the rail occupies given the widths of the cells it draws.
80
+ *
81
+ * Approximate on purpose: Yoga's own wrapping depends on margins and on how the
82
+ * terminal renders wide glyphs, and chasing exactness here is not worth it. The
83
+ * number is capped and the rail is clipped, so an under-count costs the last
84
+ * chip rather than collapsing the layout.
85
+ */
86
+ declare function measureRailRows(cells: number[] | undefined, columns: number): number;
87
+ declare function computeLayout(input: LayoutInput): Layout;
88
+
89
+ /**
90
+ * Per-agent log buffer cap, in characters.
91
+ *
92
+ * The previous web store appended output forever (`logs: a.logs + chunk`). A
93
+ * chatty agent running for hours would grow that string without bound. The TUI
94
+ * renders a tail window, so only the tail is worth keeping.
95
+ */
96
+ declare const MAX_LOG_CHARS = 8000;
97
+ interface AgentView {
98
+ id: string;
99
+ name: string;
100
+ icon?: string;
101
+ model?: string;
102
+ /** Static role, from the config. Always available, even before turn one. */
103
+ description?: string;
104
+ /** Per-turn title assigned by the leader's task plan; absent until it plans. */
105
+ taskTitle?: string;
106
+ status: AgentStatus;
107
+ /** Wall clock at which the agent started, so its timer can tick while it runs. */
108
+ startedAt?: number;
109
+ /** Final duration, present once the agent has finished. */
110
+ duration?: number;
111
+ attempts?: number;
112
+ /** In-flight retry counter, present only while the agent is between attempts. */
113
+ retry?: {
114
+ attempt: number;
115
+ max: number;
116
+ };
117
+ error?: string;
118
+ logs: string;
119
+ }
120
+ type TuiPhase = "idle" | "running" | "complete";
121
+ /**
122
+ * One turn, as the panes need to see it.
123
+ *
124
+ * Everything that describes *a turn* lives here, and everything that describes
125
+ * *the console* lives on {@link TuiState}. The split is what makes history
126
+ * possible: a finished turn is just a `TurnView` kept around, and the panes take
127
+ * a `TurnView` so they can render one without knowing which it is.
128
+ */
129
+ interface TurnView {
130
+ turn: number;
131
+ requirement?: string;
132
+ phase: TuiPhase;
133
+ /** Wall clock at which the turn started. */
134
+ startedAt?: number;
135
+ /** Wall clock at which the turn finished. */
136
+ endedAt?: number;
137
+ stages: StageInfo[];
138
+ agents: Record<string, AgentView>;
139
+ /**
140
+ * Agents that run *before* the pipeline exists (the leader), so the pipeline
141
+ * bar can label them without guessing. Derived from the config, not from
142
+ * "every agent that is not in a stage" — a configured agent that has not been
143
+ * scheduled yet is not a leader.
144
+ */
145
+ leaderIds: string[];
146
+ focusId?: string;
147
+ /**
148
+ * While true the focus pane follows whichever agent starts next. Cycling the
149
+ * focus by hand clears it, so the user's choice is not yanked away.
150
+ */
151
+ autoFollow: boolean;
152
+ /**
153
+ * How many lines the stream is scrolled back from the tail. Zero means the
154
+ * stream is live. Kept as an offset rather than an absolute index so new
155
+ * output arriving underneath does not shift what the user is reading.
156
+ */
157
+ scroll: number;
158
+ success?: boolean;
159
+ totalDuration?: number;
160
+ warning?: string;
161
+ /**
162
+ * Set when the turn ended in words rather than a pipeline: the leader answered
163
+ * or asked a question, so no agent ran. Distinct from `success`, because both
164
+ * a finished pipeline and a one-line answer are successful turns — and the
165
+ * console has to be able to say which one happened.
166
+ */
167
+ reply?: LeaderMessage;
168
+ }
169
+ /** How many finished turns the console keeps around to browse. */
170
+ declare const MAX_HISTORY = 20;
171
+ interface TuiState extends TurnView {
172
+ name: string;
173
+ /** Finished turns, oldest first. The live turn is `state` itself. */
174
+ history: TurnView[];
175
+ /**
176
+ * A finished turn opened for reading.
177
+ *
178
+ * Kept as a detached copy so scrolling and tabbing through it cannot disturb
179
+ * the turn that is actually running. While it is set the panes render it, and
180
+ * `focusId` / `scroll` edits land on it rather than on the live turn.
181
+ */
182
+ view?: TurnView;
183
+ }
184
+ declare const initialState: TuiState;
185
+ type TuiAction = {
186
+ type: "turn:start";
187
+ requirement: string;
188
+ agents: Record<string, AgentView>;
189
+ leaderIds: string[];
190
+ startedAt: number;
191
+ } | {
192
+ type: "event";
193
+ event: MultiClawEvent;
194
+ } | {
195
+ type: "focus:cycle";
196
+ delta: number;
197
+ } | {
198
+ type: "focus:set";
199
+ agentId: string;
200
+ }
201
+ /** Scroll the focused stream. `viewport` is needed to clamp at the tail. */
202
+ | {
203
+ type: "stream:scroll";
204
+ delta: number;
205
+ viewport: number;
206
+ }
207
+ /** Drop the focused agent's buffered output without touching its status. */
208
+ | {
209
+ type: "stream:clear";
210
+ }
211
+ /** Open a finished turn for reading, by its turn number. */
212
+ | {
213
+ type: "view:open";
214
+ turn: number;
215
+ }
216
+ /** Go back to the live turn. */
217
+ | {
218
+ type: "view:close";
219
+ };
220
+ declare function stripAnsi(text: string): string;
221
+ /** Append a chunk, keeping only the last {@link MAX_LOG_CHARS} characters. */
222
+ declare function appendLog(previous: string, chunk: string): string;
223
+ /** Every agent that takes part in a run, in declaration order: leader first. */
224
+ declare function plannedAgents(config: MultiClawConfig): AgentDefinition[];
225
+ declare function leaderAgentIds(config: MultiClawConfig): string[];
226
+ /**
227
+ * Build the roster shown before (and between) runs.
228
+ *
229
+ * The orchestrator only reveals the team through `orchestration:start`, which it
230
+ * emits *after* the leader has already run — so a console that learned its
231
+ * agents purely from events would show an empty list until the first turn was
232
+ * well under way. Seeding from the config makes the idle screen show the team
233
+ * you are about to run, and gives every turn a known baseline to reset to.
234
+ */
235
+ declare function seedAgents(config: MultiClawConfig): Record<string, AgentView>;
236
+ declare function createInitialState(config: MultiClawConfig): TuiState;
237
+ declare function reduceEvent(state: TuiState, event: MultiClawEvent): TuiState;
238
+ /**
239
+ * The turns behind this one, as dialogue the leader can read back.
240
+ *
241
+ * Core has no notion of a session — an orchestrator is built per turn — so the
242
+ * console is what carries earlier turns into the next run. Without them a bare
243
+ * answer arrives at the leader as a requirement with nothing to answer: turn one
244
+ * asks "which database?", turn two's requirement is the word "Postgres".
245
+ *
246
+ * Derived rather than tracked separately: `history` plus the turn on screen
247
+ * *are* the finished turns, and a second copy is one more thing that could
248
+ * disagree with what the panes show.
249
+ *
250
+ * Only turns that ended are carried, which makes the call site's timing
251
+ * irrelevant: whether this is read before or after `turn:start` moves the turn
252
+ * on screen into `history`, the set is the same.
253
+ */
254
+ declare function conversationTurns(state: TuiState): TurnRecord[];
255
+ declare function tuiReducer(state: TuiState, action: TuiAction): TuiState;
256
+ /** Roll a stage's member statuses up into one status for the pipeline bar. */
257
+ declare function stageStatus(stage: StageInfo, agents: Record<string, AgentView>): AgentStatus;
258
+ /** How many agents have finished, out of how many the turn will run. */
259
+ declare function progress(turn: TurnView): {
260
+ done: number;
261
+ total: number;
262
+ };
263
+ /**
264
+ * An agent's time: the live clock while it runs, the recorded duration once it
265
+ * has stopped. Returning the *same* number after completion is what stops the
266
+ * column from jittering when a stage ends.
267
+ */
268
+ declare function agentElapsed(agent: AgentView, now: number): number | undefined;
269
+ /** The turn's time, live while running and frozen once the orchestrator reports back. */
270
+ declare function turnElapsed(turn: TurnView, now: number): number | undefined;
271
+ interface LogWindow {
272
+ lines: string[];
273
+ /** How many lines sit below the window. Zero means the stream is live. */
274
+ scrolledBack: number;
275
+ following: boolean;
276
+ }
277
+ /**
278
+ * The slice of the focused agent's output that fits the pane.
279
+ *
280
+ * Clamping here as well as in the reducer keeps the window correct even when
281
+ * the pane is resized between two scroll actions.
282
+ */
283
+ declare function visibleLog(turn: TurnView, viewport: number): LogWindow;
284
+
285
+ interface TuiOptions {
286
+ config: MultiClawConfig;
287
+ /** Attach per-turn reporters (e.g. a file log writer) here. */
288
+ onTurnStart?: (eventBus: EventBus, logDir: string) => void;
289
+ }
290
+ /**
291
+ * Render the interactive team console.
292
+ *
293
+ * Runs on the alternate screen buffer so the app owns the viewport and the
294
+ * user's scrollback survives. The buffer is always restored — including when
295
+ * rendering throws — otherwise the terminal is left unusable.
296
+ */
297
+ declare function startTui(options: TuiOptions): Promise<void>;
298
+
299
+ export { type AgentView, type AppProps, COMPACT_COLUMNS, type Layout, type LayoutInput, type LogWindow, MAX_HISTORY, MAX_LOG_CHARS, MAX_MENU_ROWS, MAX_QUEUE_ROWS, MAX_RAIL_ROWS, MIN_COLUMNS, MIN_ROWS, RAIL_MIN_ROWS, type TuiAction, type TuiOptions, type TuiPhase, type TuiState, type TurnView, agentElapsed, appendLog, computeLayout, conversationTurns, createInitialState, initialState, leaderAgentIds, measureRailRows, plannedAgents, progress, reduceEvent, seedAgents, stageStatus, startTui, stripAnsi, tuiReducer, turnElapsed, visibleLog };