@multiclawcli/dashboard 0.5.0 → 0.6.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/dist/tui/App.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import type { EventBus, MultiClawConfig } from "@multiclawcli/core";
2
+ import { AgentSessionManager } from "@multiclawcli/core";
2
3
  export interface AppProps {
3
4
  config: MultiClawConfig;
4
5
  /**
5
6
  * Called once per turn, after that turn's orchestrator exists. The CLI uses it
6
7
  * to attach a file log writer without the TUI depending on the CLI package.
7
8
  */
8
- onTurnStart?: (eventBus: EventBus, logDir: string) => void;
9
+ onTurnStart?: (eventBus: EventBus, logDir: string) => void | (() => void | Promise<void>);
10
+ /** Caller-owned resources; startTui also closes them on render failures. */
11
+ sessions?: AgentSessionManager;
12
+ /** Gives the terminal host an awaitable cleanup barrier, including render failures. */
13
+ onCleanupReady?: (cleanup: () => Promise<void>) => void;
9
14
  }
10
- export declare function App({ config, onTurnStart }: AppProps): import("react").JSX.Element;
15
+ export declare function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }: AppProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Write JSON to `target` atomically, serialized per path.
3
+ *
4
+ * Written to a temp file and renamed into place: `rename` is atomic within a
5
+ * filesystem, so a crash mid-write leaves the *previous* file intact. Writing
6
+ * the target directly would instead leave half a JSON document, and since every
7
+ * reader here can only drop a file it cannot parse, that would cost the user
8
+ * *all* of their state rather than the last write.
9
+ *
10
+ * The returned promise rejects on failure; the serialization chain swallows it
11
+ * so one failed write cannot poison the next.
12
+ */
13
+ export declare function writeJsonFile(target: string, json: string): Promise<void>;
14
+ /**
15
+ * The last write queued for `target`, or a resolved promise when there is none.
16
+ *
17
+ * Lets a shutdown wait for state that is already on its way to disk. Without it
18
+ * the process can exit between the `writeFile` and the `rename`, which leaves
19
+ * the temporary file behind and the previous — older — file in place.
20
+ */
21
+ export declare function pendingWrite(target: string): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import type { RuntimePermissionRequest, RuntimeQuestionRequest } from "@multiclawcli/core";
1
2
  import { type TurnView } from "../state";
2
3
  interface HelpGroup {
3
4
  title: string;
@@ -43,6 +44,22 @@ export declare function HelpOverlay({ width, maxRows }: {
43
44
  width: number;
44
45
  maxRows: number;
45
46
  }): import("react").JSX.Element;
47
+ export declare function PermissionOverlay({ request, shortId, queued, width, maxRows, }: {
48
+ request: RuntimePermissionRequest;
49
+ shortId: string;
50
+ queued: number;
51
+ width: number;
52
+ maxRows: number;
53
+ }): import("react").JSX.Element;
54
+ export declare function questionLines(request: RuntimeQuestionRequest, width: number): string[];
55
+ export declare function QuestionOverlay({ request, shortId, queued, width, maxRows, offset, }: {
56
+ request: RuntimeQuestionRequest;
57
+ shortId: string;
58
+ queued: number;
59
+ width: number;
60
+ maxRows: number;
61
+ offset: number;
62
+ }): import("react").JSX.Element;
46
63
  /**
47
64
  * The finished turns, newest first.
48
65
  *
@@ -1,3 +1,4 @@
1
+ import type { SupplementReport } from "@multiclawcli/core";
1
2
  /**
2
3
  * Truncate to `max` columns, ending in an ellipsis when it does not fit.
3
4
  *
@@ -63,3 +64,21 @@ export declare function formatTokens(n: number): string;
63
64
  * than rounding it away to "$0.00" — a real but tiny cost must not read as free.
64
65
  */
65
66
  export declare function formatCost(usd: number): string;
67
+ /**
68
+ * What to tell the user after a mid-turn message.
69
+ *
70
+ * Reports are per agent and each carries its own reason, so the sentence names
71
+ * agents rather than summarising an outcome: "steered architect · leader was
72
+ * between turns" tells the user what happened to whom, while "steering failed"
73
+ * tells them nothing they can act on.
74
+ *
75
+ * `error` and `idle` must never be collapsed: the first is a delivery that went
76
+ * wrong, the second is a message that arrived at the wrong moment. Calling the
77
+ * first the second would tell the user to simply wait when they should be
78
+ * looking at a failure.
79
+ *
80
+ * When nothing was delivered the caller queues the text, so the sentence says
81
+ * so — otherwise the user is left believing their message is inside the turn
82
+ * they are watching.
83
+ */
84
+ export declare function describeSteering(reports: readonly SupplementReport[]): string;
@@ -22,11 +22,10 @@ export declare function loadHistory(workDir: string): TurnView[];
22
22
  /**
23
23
  * Write the workspace's turn history.
24
24
  *
25
- * Written to a temp file and renamed into place: `rename` is atomic within a
26
- * filesystem, so a crash mid-write leaves the *previous* file intact. Writing
27
- * `history.json` directly would instead leave half a JSON document, and since
28
- * `loadHistory` can only drop a file it cannot parse, that would cost the user
29
- * *all* of their history rather than the last write.
25
+ * Atomic and serialized per path (see {@link writeJsonFile}), because a crash
26
+ * mid-write must leave the previous history readable rather than half a
27
+ * document: `loadHistory` can only drop a file it cannot parse, so a torn write
28
+ * would cost the user *all* of their history rather than the last one.
30
29
  */
31
30
  export declare function saveHistory(workDir: string, turns: readonly TurnView[]): Promise<void>;
32
31
  export declare function exportPath(workDir: string, turn: number): string;
@@ -1,11 +1,11 @@
1
- import type { EventBus, MultiClawConfig } from "@multiclawcli/core";
1
+ import { type EventBus, type MultiClawConfig } from "@multiclawcli/core";
2
2
  export type { AppProps } from "./App";
3
3
  export * from "./layout";
4
4
  export * from "./state";
5
5
  export interface TuiOptions {
6
6
  config: MultiClawConfig;
7
7
  /** Attach per-turn reporters (e.g. a file log writer) here. */
8
- onTurnStart?: (eventBus: EventBus, logDir: string) => void;
8
+ onTurnStart?: (eventBus: EventBus, logDir: string) => void | (() => void | Promise<void>);
9
9
  }
10
10
  /**
11
11
  * Render the interactive team console.
@@ -0,0 +1,134 @@
1
+ import { type AgentView, type TuiStageInfo, type TuiState, type TurnView } from "./state";
2
+ /**
3
+ * The session journal: what a restart needs to pick the console back up.
4
+ *
5
+ * Live agent sessions are child processes, and a pending approval is an RPC in
6
+ * flight to one of them — neither survives the process that owns them. So this
7
+ * file deliberately does not try to *continue* a turn. It records the boundary
8
+ * instead: which turn was running, what the user had typed but not run, where
9
+ * the current task began, and how many decisions were waiting. A restart can
10
+ * then say what happened, show the interrupted turn as a turn, and hand the
11
+ * queued input back — without pretending a dead agent is still working.
12
+ */
13
+ export declare const SESSION_FILE = "session.json";
14
+ /** Bumped when the on-disk shape changes in a way an older console cannot read. */
15
+ export declare const SESSION_VERSION = 1;
16
+ /**
17
+ * How much of each agent's log the journal keeps.
18
+ *
19
+ * The live buffer holds `MAX_LOG_CHARS` per agent and a turn can have a dozen
20
+ * agents, so journaling it whole would rewrite a multi-hundred-kilobyte file
21
+ * every few seconds. The tail is what answers "what was it doing when it died",
22
+ * and it is the only part worth the write.
23
+ */
24
+ export declare const MAX_JOURNAL_LOG_CHARS = 1200;
25
+ /** The turn that was in flight when the journal was last written. */
26
+ export interface JournalTurn {
27
+ turn: number;
28
+ requirement: string;
29
+ startedAt?: number;
30
+ effectiveRequirement?: string;
31
+ reason?: string;
32
+ warning?: string;
33
+ /**
34
+ * Wall clock of this write — the last moment the console was known to be
35
+ * alive. The restored turn ends here, so its duration reads as "how long it
36
+ * ran", not "how long ago it was".
37
+ */
38
+ savedAt: number;
39
+ leaderIds: string[];
40
+ focusId?: string;
41
+ stages: TuiStageInfo[];
42
+ agents: Record<string, AgentView>;
43
+ /** Decisions the agents were waiting on. A restart cannot answer them. */
44
+ pending?: {
45
+ permissions: number;
46
+ questions: number;
47
+ };
48
+ }
49
+ export interface SessionJournal {
50
+ version: number;
51
+ /**
52
+ * Turn number the current task began at.
53
+ *
54
+ * `/task` closes the previous task's context, and that boundary has to outlive
55
+ * the process: without it a restart would hand the leader every turn ever run
56
+ * in the workspace, including the ones the user explicitly closed.
57
+ */
58
+ taskStart?: number;
59
+ /** Follow-ups typed but never run, in the order they were typed. */
60
+ queue?: string[];
61
+ inFlight?: JournalTurn;
62
+ }
63
+ /** Where the console keeps its session journal for a workspace. */
64
+ export declare function sessionPath(workDir: string): string;
65
+ /**
66
+ * Read the workspace's session journal.
67
+ *
68
+ * Never throws, for the same reason `loadHistory` does not: a corrupted side
69
+ * file means "no journal", and a console that refuses to start over it would be
70
+ * far worse than one that starts with an empty past. A journal written by a
71
+ * newer console (unknown `version`) is ignored rather than guessed at.
72
+ */
73
+ export declare function loadJournal(workDir: string): SessionJournal | undefined;
74
+ /**
75
+ * Write the workspace's session journal.
76
+ *
77
+ * Atomic and serialized per path (see {@link writeJsonFile}): the whole point is
78
+ * to survive a process that dies mid-write, so the write itself must never be
79
+ * the thing that leaves a torn file behind.
80
+ *
81
+ * An empty journal does not create the file. A console that was opened and quit
82
+ * without doing anything should not leave `.multiclaw/console/` behind — but an
83
+ * *existing* journal is still rewritten when it empties, because "nothing is in
84
+ * flight" is exactly what the next start needs to read.
85
+ */
86
+ export declare function saveJournal(workDir: string, journal: SessionJournal): Promise<void>;
87
+ /**
88
+ * The journal entry for a turn that is running right now.
89
+ *
90
+ * A pure projection of what the panes are showing, so the journal can never
91
+ * describe a turn the console is not actually running. Logs are trimmed; every
92
+ * other field is small enough to keep whole.
93
+ */
94
+ export declare function journalTurn(state: TuiState, pending: {
95
+ permissions: number;
96
+ questions: number;
97
+ }): JournalTurn;
98
+ /**
99
+ * Rebuild an interrupted turn as a browsable record.
100
+ *
101
+ * Two deliberate rewrites, both because the record must not claim work that is
102
+ * not happening:
103
+ *
104
+ * - An agent that never reported back becomes `cancelled`. Leaving it `running`
105
+ * would animate a spinner and a growing clock for a process that died with
106
+ * the console.
107
+ * - Stage outcomes are dropped so they are re-derived from those statuses. A
108
+ * stage that had finished before the crash keeps its success, and one that was
109
+ * mid-flight reads as cancelled instead of frozen at `running` for ever.
110
+ *
111
+ * The turn itself is `complete` — nothing will be added to it — with
112
+ * `interrupted` set so every surface can say *how* it ended.
113
+ */
114
+ export declare function interruptedTurn(entry: JournalTurn): TurnView;
115
+ export interface RestoredConsole {
116
+ history: TurnView[];
117
+ queue: string[];
118
+ taskStart: number;
119
+ /** The last turn ended by asking the user something, so the console reopens ready for it. */
120
+ awaitingAnswer: boolean;
121
+ /** One line for the notice bar, when the previous session left work behind. */
122
+ notice?: string;
123
+ }
124
+ /**
125
+ * Everything the console should open with, read once at startup.
126
+ *
127
+ * The interrupted turn is appended to the restored history rather than replacing
128
+ * it: the turns before it still happened. It is skipped when history already
129
+ * holds that turn number, because a restored turn is written back to
130
+ * `history.json` on the very next save — a second restart must not add it twice.
131
+ * A journal *behind* the last recorded turn is stale (the history was written by
132
+ * a later session) and is dropped for the same reason.
133
+ */
134
+ export declare function restoreConsole(workDir: string): RestoredConsole;
@@ -1,4 +1,4 @@
1
- import type { AgentDefinition, AgentOutput, AgentStatus, LeaderMessage, MultiClawConfig, MultiClawEvent, StageInfo, TurnRecord } from "@multiclawcli/core";
1
+ import type { AcceptanceResult, AgentDefinition, AgentOutput, AgentStatus, LeaderMessage, MultiClawConfig, MultiClawEvent, StageInfo, TurnRecord } from "@multiclawcli/core";
2
2
  /**
3
3
  * Per-agent log buffer cap, in characters.
4
4
  *
@@ -41,6 +41,11 @@ export interface AgentView {
41
41
  logs: string;
42
42
  }
43
43
  export type TuiPhase = "idle" | "running" | "complete";
44
+ export interface TuiStageInfo extends StageInfo {
45
+ /** Frozen when this stage finishes, independent of later runs of its agents. */
46
+ outcome?: AgentStatus;
47
+ memberStatuses?: Record<string, AgentStatus>;
48
+ }
44
49
  /**
45
50
  * One turn, as the panes need to see it.
46
51
  *
@@ -52,12 +57,14 @@ export type TuiPhase = "idle" | "running" | "complete";
52
57
  export interface TurnView {
53
58
  turn: number;
54
59
  requirement?: string;
60
+ effectiveRequirement?: string;
61
+ cancelled?: boolean;
55
62
  phase: TuiPhase;
56
63
  /** Wall clock at which the turn started. */
57
64
  startedAt?: number;
58
65
  /** Wall clock at which the turn finished. */
59
66
  endedAt?: number;
60
- stages: StageInfo[];
67
+ stages: TuiStageInfo[];
61
68
  agents: Record<string, AgentView>;
62
69
  /**
63
70
  * Agents that run *before* the pipeline exists (the leader), so the pipeline
@@ -82,6 +89,19 @@ export interface TurnView {
82
89
  */
83
90
  scroll: number;
84
91
  success?: boolean;
92
+ /**
93
+ * Set on a turn that was recovered from a journal after the console died
94
+ * mid-run.
95
+ *
96
+ * Such a turn is `complete` — it is over, and its record is final — but it
97
+ * did not *finish*, so it must not be reported as a success or a failure.
98
+ * `cancelled` is deliberately not reused: that means the user stopped the
99
+ * run, while this one was stopped by the process going away, and the console
100
+ * has to be able to tell the two apart when the user asks "what happened to
101
+ * turn 3".
102
+ */
103
+ interrupted?: boolean;
104
+ acceptance?: AcceptanceResult;
85
105
  totalDuration?: number;
86
106
  /**
87
107
  * Sum of the per-agent costs the runtime reported, in USD. Absent when none
@@ -166,6 +186,18 @@ export type TuiAction = {
166
186
  | {
167
187
  type: "stream:clear";
168
188
  }
189
+ /**
190
+ * Append a console marker to one agent's stream — what the user did, not what
191
+ * the agent said.
192
+ *
193
+ * Always the live turn: a mid-turn message can only reach what is running
194
+ * now, so there is no past turn for it to belong to.
195
+ */
196
+ | {
197
+ type: "stream:note";
198
+ agentId: string;
199
+ text: string;
200
+ }
169
201
  /** Open a finished turn for reading, by its turn number. */
170
202
  | {
171
203
  type: "view:open";
@@ -244,6 +276,12 @@ export declare function recordedTurns(state: TuiState): TurnView[];
244
276
  * Only turns that ended are carried, which makes the call site's timing
245
277
  * irrelevant: whether this is read before or after `turn:start` moves the turn
246
278
  * on screen into `history`, the set is the same.
279
+ *
280
+ * An *interrupted* turn is carried even though it has no reply. A cancelled one
281
+ * is not: the user chose to abandon it. An interrupted turn was abandoned by
282
+ * the process, so its requirement is unfinished business — and it is the only
283
+ * thing that makes a follow-up like "carry on with that" readable after a
284
+ * restart.
247
285
  */
248
286
  export declare function conversationTurns(state: TuiState): TurnRecord[];
249
287
  export declare function tuiReducer(state: TuiState, action: TuiAction): TuiState;
@@ -257,7 +295,7 @@ export declare function tuiReducer(state: TuiState, action: TuiAction): TuiState
257
295
  * to start, and reporting it as `pending` would be the same lie B2-6 fixed one
258
296
  * level down.
259
297
  */
260
- export declare function stageStatus(stage: StageInfo, agents: Record<string, AgentView>): AgentStatus;
298
+ export declare function stageStatus(stage: TuiStageInfo, agents: Record<string, AgentView>): AgentStatus;
261
299
  /** How many agents have finished, out of how many the turn will run. */
262
300
  export declare function progress(turn: TurnView): {
263
301
  done: number;