@multiclawcli/dashboard 0.8.2 → 0.10.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,5 +1,19 @@
1
1
  import type { EventBus, MultiClawConfig } from "@multiclawcli/core";
2
2
  import { AgentSessionManager } from "@multiclawcli/core";
3
+ /**
4
+ * How the console should choose its session on launch.
5
+ *
6
+ * Absent means the default: continue the most recent session, or start one when
7
+ * the workspace has none. `new` always starts fresh. `resume` opens the named
8
+ * session, or — with no id — the most recent one with the picker already open,
9
+ * so `multiclaw -r` lands on the list the way `claude -r` does.
10
+ */
11
+ export type InitialSession = {
12
+ kind: "new";
13
+ } | {
14
+ kind: "resume";
15
+ id?: string;
16
+ };
3
17
  export interface AppProps {
4
18
  config: MultiClawConfig;
5
19
  /**
@@ -11,5 +25,7 @@ export interface AppProps {
11
25
  sessions?: AgentSessionManager;
12
26
  /** Gives the terminal host an awaitable cleanup barrier, including render failures. */
13
27
  onCleanupReady?: (cleanup: () => Promise<void>) => void;
28
+ /** How to choose the session on launch; defaults to continuing the latest. */
29
+ initialSession?: InitialSession;
14
30
  }
15
- export declare function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }: AppProps): import("react").JSX.Element;
31
+ export declare function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady, initialSession, }: AppProps): import("react").JSX.Element;
@@ -19,3 +19,13 @@ export declare function writeJsonFile(target: string, json: string): Promise<voi
19
19
  * the temporary file behind and the previous — older — file in place.
20
20
  */
21
21
  export declare function pendingWrite(target: string): Promise<void>;
22
+ /**
23
+ * Append newline-terminated lines to `target`, serialized per path on the same
24
+ * chain as {@link writeJsonFile}.
25
+ *
26
+ * Unlike the rewrite path this does not go through a temp file: an append is the
27
+ * cheap O(1) write a JSONL log wants, and a crash mid-append can only tear the
28
+ * *last* line, which every line-oriented reader here already skips. Sharing the
29
+ * `writes` chain keeps a `pendingWrite` on the same path able to await it.
30
+ */
31
+ export declare function appendLines(target: string, lines: readonly string[]): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import type { SessionMeta } from "../sessions";
1
2
  import { type TurnView } from "../state";
2
3
  interface HelpGroup {
3
4
  title: string;
@@ -61,4 +62,22 @@ export declare function HistoryOverlay({ turns, selected, width, maxRows, }: {
61
62
  /** Rows available in the body. The list is windowed against this. */
62
63
  maxRows: number;
63
64
  }): import("react").JSX.Element;
65
+ /**
66
+ * The session picker: every session in the workspace, newest activity first.
67
+ *
68
+ * The level above {@link HistoryOverlay}. Where the history list browses the
69
+ * turns *within* a session, this switches between whole sessions — the thing a
70
+ * restart, a `/new`, or opening the console again all create. The one the
71
+ * console is attached to is marked, because "which am I on" is the first
72
+ * question the list has to answer.
73
+ */
74
+ export declare function SessionsOverlay({ sessions, selected, currentId, width, maxRows, now, }: {
75
+ sessions: SessionMeta[];
76
+ selected: number;
77
+ currentId: string | undefined;
78
+ width: number;
79
+ /** Rows available in the body. The list is windowed against this. */
80
+ maxRows: number;
81
+ now: number;
82
+ }): import("react").JSX.Element;
64
83
  export {};
@@ -43,6 +43,14 @@ export declare function wrapLine(line: string, width: number): string[];
43
43
  * scan a stack of durations only if they share a shape.
44
44
  */
45
45
  export declare function formatDuration(ms: number): string;
46
+ /**
47
+ * A coarse "how long ago", for the session list.
48
+ *
49
+ * Deliberately low-resolution — the picker wants "which one did I touch last",
50
+ * not a precise clock. Future timestamps (a clock that moved backwards) read as
51
+ * "just now" rather than a negative age.
52
+ */
53
+ export declare function formatAgo(timestamp: number, now: number): string;
46
54
  /** Stage markers read as an ordered sequence at a glance; past twenty, fall back. */
47
55
  export declare function circled(n: number): string;
48
56
  /** Pad to a fixed column so sibling rows line up. */
@@ -1,14 +1,12 @@
1
1
  import { type TurnView } from "./state";
2
- /** Directory, under a workspace's `.multiclaw/`, that holds console state. */
3
- export declare const CONSOLE_DIR = "console";
4
- /** The finished-turn record the console keeps across sessions. */
5
- export declare const HISTORY_FILE = "history.json";
2
+ /** The finished-turn log a session keeps, one JSON turn per line. */
3
+ export declare const TURNS_FILE = "turns.jsonl";
6
4
  /** Where exported turn markdown files are written by default. */
7
5
  export declare const EXPORT_DIR = "exports";
8
- /** Where the console keeps its turn history for a workspace. */
9
- export declare function historyPath(workDir: string): string;
6
+ /** Where a session keeps its finished turns. */
7
+ export declare function turnsPath(workDir: string, sessionId: string): string;
10
8
  /**
11
- * Read the workspace's turn history.
9
+ * Read a session's turn history.
12
10
  *
13
11
  * Synchronous on purpose: it runs once, inside the reducer's lazy initialiser,
14
12
  * before the first frame. An async load would race the user's first keystroke
@@ -16,18 +14,21 @@ export declare function historyPath(workDir: string): string;
16
14
  *
17
15
  * Never throws. A missing directory, malformed JSON, or a value of the wrong
18
16
  * shape all mean "no history": the console can always start from empty, and
19
- * crashing on startup because a side file got corrupted would be far worse.
17
+ * crashing on startup because a side file got corrupted would be far worse. The
18
+ * log is line-oriented, so a single torn line (a crash mid-append) is skipped
19
+ * rather than costing the whole session's history.
20
20
  */
21
- export declare function loadHistory(workDir: string): TurnView[];
21
+ export declare function loadTurns(workDir: string, sessionId: string): TurnView[];
22
22
  /**
23
- * Write the workspace's turn history.
23
+ * Append finished turns to a session's log, one JSON object per line.
24
24
  *
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.
25
+ * Append-only and serialized per path (see {@link appendLines}): each settled
26
+ * turn is an O(1) write rather than a rewrite of the whole history, and a crash
27
+ * mid-append can only tear the last line, which {@link loadTurns} skips. Callers
28
+ * pass only the turns not yet persisted, tracked by the highest turn number
29
+ * already written.
29
30
  */
30
- export declare function saveHistory(workDir: string, turns: readonly TurnView[]): Promise<void>;
31
- export declare function exportPath(workDir: string, turn: number): string;
31
+ export declare function appendTurns(workDir: string, sessionId: string, turns: readonly TurnView[]): Promise<void>;
32
+ export declare function exportPath(workDir: string, sessionId: string, turn: number): string;
32
33
  export declare function exportTurn(turn: TurnView, filePath: string): Promise<void>;
33
34
  export declare function formatTurnMarkdown(turn: TurnView): string;
@@ -1,11 +1,14 @@
1
1
  import { type EventBus, type MultiClawConfig } from "@multiclawcli/core";
2
- export type { AppProps } from "./App";
2
+ import { type InitialSession } from "./App";
3
+ export type { AppProps, InitialSession } from "./App";
3
4
  export * from "./layout";
4
5
  export * from "./state";
5
6
  export interface TuiOptions {
6
7
  config: MultiClawConfig;
7
8
  /** Attach per-turn reporters (e.g. a file log writer) here. */
8
9
  onTurnStart?: (eventBus: EventBus, logDir: string) => void | (() => void | Promise<void>);
10
+ /** How to choose the session on launch; defaults to continuing the latest. */
11
+ initialSession?: InitialSession;
9
12
  }
10
13
  /**
11
14
  * Render the interactive team console.
@@ -5,10 +5,10 @@ import { type AgentView, type TuiStageInfo, type TuiState, type TurnView } from
5
5
  * Live agent sessions are child processes, and a pending approval is an RPC in
6
6
  * flight to one of them — neither survives the process that owns them. So this
7
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.
8
+ * instead: which turn was running, what the user had typed but not run, and how
9
+ * many decisions were waiting. A restart can then say what happened, show the
10
+ * interrupted turn as a turn, and hand the queued input back — without
11
+ * pretending a dead agent is still working.
12
12
  */
13
13
  export declare const SESSION_FILE = "session.json";
14
14
  /** Bumped when the on-disk shape changes in a way an older console cannot read. */
@@ -48,29 +48,21 @@ export interface JournalTurn {
48
48
  }
49
49
  export interface SessionJournal {
50
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
51
  /** Follow-ups typed but never run, in the order they were typed. */
60
52
  queue?: string[];
61
53
  inFlight?: JournalTurn;
62
54
  }
63
- /** Where the console keeps its session journal for a workspace. */
64
- export declare function sessionPath(workDir: string): string;
55
+ /** Where a session keeps its journal. */
56
+ export declare function sessionPath(workDir: string, sessionId: string): string;
65
57
  /**
66
- * Read the workspace's session journal.
58
+ * Read a session's journal.
67
59
  *
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.
60
+ * Never throws, for the same reason `loadTurns` does not: a corrupted side file
61
+ * means "no journal", and a console that refuses to start over it would be far
62
+ * worse than one that starts with an empty past. A journal written by a newer
63
+ * console (unknown `version`) is ignored rather than guessed at.
72
64
  */
73
- export declare function loadJournal(workDir: string): SessionJournal | undefined;
65
+ export declare function loadJournal(workDir: string, sessionId: string): SessionJournal | undefined;
74
66
  /**
75
67
  * Write the workspace's session journal.
76
68
  *
@@ -78,12 +70,12 @@ export declare function loadJournal(workDir: string): SessionJournal | undefined
78
70
  * to survive a process that dies mid-write, so the write itself must never be
79
71
  * the thing that leaves a torn file behind.
80
72
  *
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.
73
+ * An empty journal does not create the file. A session that was opened and quit
74
+ * without doing anything should not leave a journal behind — but an *existing*
75
+ * journal is still rewritten when it empties, because "nothing is in flight" is
76
+ * exactly what the next start needs to read.
85
77
  */
86
- export declare function saveJournal(workDir: string, journal: SessionJournal): Promise<void>;
78
+ export declare function saveJournal(workDir: string, sessionId: string, journal: SessionJournal): Promise<void>;
87
79
  /**
88
80
  * The journal entry for a turn that is running right now.
89
81
  *
@@ -115,20 +107,25 @@ export declare function interruptedTurn(entry: JournalTurn): TurnView;
115
107
  export interface RestoredConsole {
116
108
  history: TurnView[];
117
109
  queue: string[];
118
- taskStart: number;
110
+ /**
111
+ * Highest turn number already on disk in `turns.jsonl` — the watermark the
112
+ * console appends past. Excludes a journal-recovered interrupted turn, which
113
+ * lives only in the journal until the next save writes it to the log.
114
+ */
115
+ persistedTurn: number;
119
116
  /** The last turn ended by asking the user something, so the console reopens ready for it. */
120
117
  awaitingAnswer: boolean;
121
118
  /** One line for the notice bar, when the previous session left work behind. */
122
119
  notice?: string;
123
120
  }
124
121
  /**
125
- * Everything the console should open with, read once at startup.
122
+ * Everything the console should open a session with, read once at startup.
126
123
  *
127
124
  * 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.
125
+ * it: the turns before it still happened. It is skipped when the log already
126
+ * holds that turn number, because a restored turn is written to the log on the
127
+ * very next save — a second restart must not add it twice. A journal *behind*
128
+ * the last recorded turn is stale (the log was written by a later session) and
129
+ * is dropped for the same reason.
133
130
  */
134
- export declare function restoreConsole(workDir: string): RestoredConsole;
131
+ export declare function restoreConsole(workDir: string, sessionId: string): RestoredConsole;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Sessions: the unit of history and resumption.
3
+ *
4
+ * A session is one continuous line of work — a sequence of turns, each fanning
5
+ * out to the same team of agents. It is what Claude Code / Codex call a session:
6
+ * an independent, listable, resumable, deletable record. Everything a session
7
+ * owns lives under its own directory (`turns.jsonl`, `session.json`, exports),
8
+ * and a small `index.json` holds just the summaries the picker needs so it never
9
+ * has to open a session to list one.
10
+ */
11
+ export declare const SESSIONS_DIR = "sessions";
12
+ /** The summary index, read by the picker; one entry per session. */
13
+ export declare const INDEX_FILE = "index.json";
14
+ /** How long a session title may grow before it is truncated for the index. */
15
+ export declare const MAX_TITLE_CHARS = 80;
16
+ export interface SessionMeta {
17
+ id: string;
18
+ /** First turn's requirement, truncated. Empty until the session runs a turn. */
19
+ title: string;
20
+ createdAt: number;
21
+ lastActiveAt: number;
22
+ /** Highest turn number the session has recorded. */
23
+ turnCount: number;
24
+ }
25
+ export declare function sessionsRoot(workDir: string): string;
26
+ export declare function sessionDir(workDir: string, id: string): string;
27
+ /**
28
+ * Read the session index, newest-first by last activity.
29
+ *
30
+ * Never throws, like `loadTurns`: a missing or corrupt index means "no sessions
31
+ * the picker can show", which is always a safe place to start from. A single bad
32
+ * entry is dropped rather than losing the whole list.
33
+ */
34
+ export declare function listSessions(workDir: string): SessionMeta[];
35
+ /** The most recently active session, or undefined when there is none. */
36
+ export declare function latestSession(workDir: string): SessionMeta | undefined;
37
+ /**
38
+ * Register a new, empty session and return its metadata.
39
+ *
40
+ * The directory is not created here — `appendTurns` and `saveJournal` both
41
+ * `mkdir` lazily, so a session that is created and never run leaves nothing but
42
+ * an index entry behind.
43
+ */
44
+ export declare function createSession(workDir: string): SessionMeta;
45
+ /** Remove a session's directory and its index entry. Idempotent. */
46
+ export declare function deleteSession(workDir: string, id: string): void;
47
+ /** Update a session's summary in the index. `lastActiveAt` defaults to now. */
48
+ export declare function touchSession(workDir: string, id: string, patch: Partial<Pick<SessionMeta, "title" | "turnCount" | "lastActiveAt">>): void;
@@ -210,6 +210,27 @@ export type TuiAction = {
210
210
  /** Flip whether tool detail is shown in the stream. */
211
211
  | {
212
212
  type: "verbose:toggle";
213
+ }
214
+ /**
215
+ * Start a fresh session in place: empty history, turn counter back to zero,
216
+ * roster reseeded. Carries the seeded agents and leader ids the same way
217
+ * `turn:start` does, so no module-level object is shared into live state.
218
+ */
219
+ | {
220
+ type: "session:reset";
221
+ agents: Record<string, AgentView>;
222
+ leaderIds: string[];
223
+ }
224
+ /**
225
+ * Switch to an existing session: like `session:reset`, but seeded with that
226
+ * session's finished turns so its history is on screen and the turn counter
227
+ * continues from where it left off.
228
+ */
229
+ | {
230
+ type: "session:load";
231
+ agents: Record<string, AgentView>;
232
+ leaderIds: string[];
233
+ history: TurnView[];
213
234
  };
214
235
  export declare function stripAnsi(text: string): string;
215
236
  /** Append a chunk, keeping only the last {@link MAX_LOG_CHARS} characters. */