@multiclawcli/dashboard 0.10.1 → 0.12.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.
@@ -1,5 +1,4 @@
1
1
  import type { SessionMeta } from "../sessions";
2
- import { type TurnView } from "../state";
3
2
  interface HelpGroup {
4
3
  title: string;
5
4
  rows: Array<[keys: string, description: string]>;
@@ -16,8 +15,8 @@ interface HelpGroup {
16
15
  * enumeration belongs.
17
16
  */
18
17
  export declare const HELP_GROUPS: readonly HelpGroup[];
19
- /** How many turns fit in a body of `maxRows`. Never fewer than one. */
20
- export declare function historyCapacity(maxRows: number): number;
18
+ /** How many items fit in a body of `maxRows`. Never fewer than one. */
19
+ export declare function overlayCapacity(maxRows: number): number;
21
20
  /**
22
21
  * The first item to draw, given the one the cursor is on.
23
22
  *
@@ -44,32 +43,12 @@ export declare function HelpOverlay({ width, maxRows }: {
44
43
  width: number;
45
44
  maxRows: number;
46
45
  }): import("react").JSX.Element;
47
- /**
48
- * The finished turns, newest first.
49
- *
50
- * Two rows per turn on purpose: the requirement is the thing a reader is
51
- * scanning for, and folding it onto the same line as the outcome would truncate
52
- * exactly the part that identifies the turn.
53
- *
54
- * Only turns a newer turn has superseded are listed — the one being run is
55
- * already on screen, so listing it would offer to show the user what they are
56
- * looking at.
57
- */
58
- export declare function HistoryOverlay({ turns, selected, width, maxRows, }: {
59
- turns: TurnView[];
60
- selected: number;
61
- width: number;
62
- /** Rows available in the body. The list is windowed against this. */
63
- maxRows: number;
64
- }): import("react").JSX.Element;
65
46
  /**
66
47
  * The session picker: every session in the workspace, newest activity first.
67
48
  *
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.
49
+ * Switches between whole sessions — the thing a restart, a `/new`, or opening
50
+ * the console again all create. The one the console is attached to is marked,
51
+ * because "which am I on" is the first question the list has to answer.
73
52
  */
74
53
  export declare function SessionsOverlay({ sessions, selected, currentId, width, maxRows, now, }: {
75
54
  sessions: SessionMeta[];
@@ -1,4 +1,15 @@
1
1
  import type { SupplementReport } from "@multiclawcli/core";
2
+ import type { LogSegment } from "./state";
3
+ /**
4
+ * Render a structured log buffer to a single ANSI string ready for `wrapLog`.
5
+ *
6
+ * - `text` segments are rendered as markdown via md4x (with `heal:true` to
7
+ * handle streaming-incomplete syntax gracefully).
8
+ * - `tool` segments are prefixed with a dim-cyan glyph.
9
+ * - `system` segments (errors, retries, steer notes) are rendered dim yellow.
10
+ * - `tool-detail` segments are already filtered at storage time; skipped here.
11
+ */
12
+ export declare function renderSegments(segments: LogSegment[], _width: number): string;
2
13
  /**
3
14
  * Truncate to `max` columns, ending in an ellipsis when it does not fit.
4
15
  *
@@ -7,6 +7,11 @@ import type { AcceptanceResult, AgentDefinition, AgentOutput, AgentStatus, Leade
7
7
  * renders a tail window, so only the tail is worth keeping.
8
8
  */
9
9
  export declare const MAX_LOG_CHARS = 8000;
10
+ export type LogKind = "text" | "tool" | "tool-detail" | "system";
11
+ export interface LogSegment {
12
+ kind: LogKind;
13
+ text: string;
14
+ }
10
15
  export interface AgentView {
11
16
  id: string;
12
17
  name: string;
@@ -38,7 +43,7 @@ export interface AgentView {
38
43
  * re-exploring from scratch.
39
44
  */
40
45
  sessionId?: string;
41
- logs: string;
46
+ logs: LogSegment[];
42
47
  }
43
48
  export type TuiPhase = "idle" | "running" | "complete";
44
49
  export interface TuiStageInfo extends StageInfo {
@@ -234,7 +239,7 @@ export type TuiAction = {
234
239
  };
235
240
  export declare function stripAnsi(text: string): string;
236
241
  /** Append a chunk, keeping only the last {@link MAX_LOG_CHARS} characters. */
237
- export declare function appendLog(previous: string, chunk: string): string;
242
+ export declare function appendLog(previous: LogSegment[], chunk: string, kind?: LogKind): LogSegment[];
238
243
  /** Every agent that takes part in a run, in declaration order: leader first. */
239
244
  export declare function plannedAgents(config: MultiClawConfig): AgentDefinition[];
240
245
  export declare function leaderAgentIds(config: MultiClawConfig): string[];
package/dist/tui.js CHANGED
@@ -66,11 +66,6 @@ var COMMANDS = [
66
66
  completes: "agent"
67
67
  },
68
68
  { name: "clear", description: "Clear the focused agent's log", availability: "always" },
69
- {
70
- name: "history",
71
- description: "Browse finished turns, or continue one",
72
- availability: "always"
73
- },
74
69
  {
75
70
  name: "sessions",
76
71
  description: "Switch between, or delete, saved sessions",
@@ -260,7 +255,35 @@ function resolveAgentId(input, ids) {
260
255
  import { Box, Text } from "ink";
261
256
 
262
257
  // src/tui/format.ts
258
+ import { renderToAnsi } from "md4x/napi";
263
259
  import stringWidth from "string-width";
260
+ var DIM = "\x1B[2m";
261
+ var CYAN = "\x1B[36m";
262
+ var YELLOW = "\x1B[33m";
263
+ var RESET = "\x1B[0m";
264
+ function renderSegments(segments, _width) {
265
+ const parts = [];
266
+ let textBuf = "";
267
+ const flushText = () => {
268
+ if (!textBuf) return;
269
+ parts.push(renderToAnsi(textBuf, { heal: true }));
270
+ textBuf = "";
271
+ };
272
+ for (const seg of segments) {
273
+ if (seg.kind === "text") {
274
+ textBuf += seg.text;
275
+ } else {
276
+ flushText();
277
+ if (seg.kind === "tool") {
278
+ parts.push(`${DIM}${CYAN}\u25B8 ${seg.text}${RESET}`);
279
+ } else if (seg.kind === "system") {
280
+ parts.push(`${DIM}${YELLOW}${seg.text}${RESET}`);
281
+ }
282
+ }
283
+ }
284
+ flushText();
285
+ return parts.join("\n");
286
+ }
264
287
  function truncate(text, max) {
265
288
  if (max <= 0) return "";
266
289
  if (text.length <= max) return text;
@@ -650,12 +673,29 @@ var initialState = {
650
673
  function stripAnsi(text) {
651
674
  return text.replace(ANSI_PATTERN, "");
652
675
  }
653
- function appendLog(previous, chunk) {
654
- const next = previous + stripAnsi(chunk);
655
- return next.length > MAX_LOG_CHARS ? next.slice(next.length - MAX_LOG_CHARS) : next;
676
+ function appendLog(previous, chunk, kind = "text") {
677
+ const text = stripAnsi(chunk);
678
+ if (!text) return previous;
679
+ const last = previous[previous.length - 1];
680
+ const next = last?.kind === kind ? [...previous.slice(0, -1), { kind, text: last.text + text }] : [...previous, { kind, text }];
681
+ let total = next.reduce((sum, s) => sum + s.text.length, 0);
682
+ if (total <= MAX_LOG_CHARS) return next;
683
+ const result = [...next];
684
+ while (total > MAX_LOG_CHARS && result.length > 0) {
685
+ const first = result[0];
686
+ const excess = total - MAX_LOG_CHARS;
687
+ if (first.text.length <= excess) {
688
+ total -= first.text.length;
689
+ result.shift();
690
+ } else {
691
+ result[0] = { ...first, text: first.text.slice(excess) };
692
+ break;
693
+ }
694
+ }
695
+ return result;
656
696
  }
657
697
  function blankAgent(id, name, patch = {}) {
658
- return { id, name, status: "pending", logs: "", ...patch };
698
+ return { id, name, status: "pending", logs: [], ...patch };
659
699
  }
660
700
  function epoch(iso) {
661
701
  const value = Date.parse(iso);
@@ -758,14 +798,14 @@ function reduceEvent(state, event) {
758
798
  case "stage:complete": {
759
799
  const stage = state.stages.find((stage2) => stage2.stageIndex === event.payload.stageIndex);
760
800
  if (!stage || stage.outcome !== void 0) return state;
761
- const outcome2 = stageStatus(stage, state.agents);
801
+ const outcome = stageStatus(stage, state.agents);
762
802
  const memberStatuses = Object.fromEntries(
763
803
  stage.agents.map((agent) => [agent.id, state.agents[agent.id]?.status ?? "pending"])
764
804
  );
765
805
  return {
766
806
  ...state,
767
807
  stages: state.stages.map(
768
- (entry) => entry === stage ? { ...entry, outcome: outcome2, memberStatuses } : entry
808
+ (entry) => entry === stage ? { ...entry, outcome, memberStatuses } : entry
769
809
  )
770
810
  };
771
811
  }
@@ -787,7 +827,7 @@ function reduceEvent(state, event) {
787
827
  error: void 0,
788
828
  usage: void 0,
789
829
  costUsd: void 0,
790
- logs: existing.startedAt !== void 0 && existing.logs ? appendLog(existing.logs, "\n[agent restarted]\n") : existing.logs
830
+ logs: existing.startedAt !== void 0 && existing.logs.length > 0 ? appendLog(existing.logs, "[agent restarted]", "system") : existing.logs
791
831
  } : blankAgent(agentId, agentName, {
792
832
  icon,
793
833
  model,
@@ -813,7 +853,7 @@ function reduceEvent(state, event) {
813
853
  ...state,
814
854
  agents: {
815
855
  ...state.agents,
816
- [agentId]: { ...existing, logs: appendLog(existing.logs, chunk) }
856
+ [agentId]: { ...existing, logs: appendLog(existing.logs, chunk, kind ?? "text") }
817
857
  }
818
858
  };
819
859
  }
@@ -835,11 +875,10 @@ function reduceEvent(state, event) {
835
875
  const existing = state.agents[result.agentId];
836
876
  const next = existing ? { ...existing, ...patch } : blankAgent(result.agentId, result.agentName, patch);
837
877
  if (event.type === "agent:failed") {
838
- if (next.logs === "" && result.output) next.logs = appendLog("", result.output);
839
- if (result.error && !next.logs.includes(stripAnsi(result.error))) {
840
- next.logs = appendLog(next.logs, `
841
- [error] ${result.error}
842
- `);
878
+ if (next.logs.length === 0 && result.output)
879
+ next.logs = appendLog([], result.output, "text");
880
+ if (result.error && !next.logs.some((s) => s.text.includes(stripAnsi(result.error)))) {
881
+ next.logs = appendLog(next.logs, result.error, "system");
843
882
  }
844
883
  }
845
884
  return { ...state, agents: { ...state.agents, [result.agentId]: next } };
@@ -852,12 +891,14 @@ function reduceEvent(state, event) {
852
891
  }
853
892
  case "agent:retrying": {
854
893
  const { agentId, agentName, attempt, maxAttempts, error } = event.payload;
855
- const note = `
856
- [retry ${attempt}/${maxAttempts}] ${error}
857
- `;
894
+ const note = `[retry ${attempt}/${maxAttempts}] ${error}`;
858
895
  const existing = state.agents[agentId];
859
896
  const retry = { attempt, max: maxAttempts };
860
- const next = existing ? { ...existing, status: "retrying", retry, logs: appendLog(existing.logs, note) } : blankAgent(agentId, agentName, { status: "retrying", retry, logs: note });
897
+ const next = existing ? { ...existing, status: "retrying", retry, logs: appendLog(existing.logs, note, "system") } : blankAgent(agentId, agentName, {
898
+ status: "retrying",
899
+ retry,
900
+ logs: appendLog([], note, "system")
901
+ });
861
902
  return { ...state, agents: { ...state.agents, [agentId]: next } };
862
903
  }
863
904
  case "orchestration:warning":
@@ -880,7 +921,9 @@ function reduceEvent(state, event) {
880
921
  }
881
922
  }
882
923
  function wrapLog(logs, width) {
883
- return logs.split("\n").flatMap((line) => wrapLine(line, width));
924
+ if (logs.length === 0) return [];
925
+ const rendered = renderSegments(logs, width);
926
+ return rendered.trimEnd().split("\n").flatMap((line) => wrapLine(line, width));
884
927
  }
885
928
  function focusedRowCount(turn, width) {
886
929
  const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
@@ -1052,7 +1095,7 @@ function tuiReducer(state, action) {
1052
1095
  const id = turn.focusId;
1053
1096
  const agent = id ? turn.agents[id] : void 0;
1054
1097
  if (!id || !agent) return turn;
1055
- return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: "" } }, scroll: 0 };
1098
+ return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: [] } }, scroll: 0 };
1056
1099
  });
1057
1100
  case "stream:note": {
1058
1101
  const agent = state.agents[action.agentId];
@@ -1061,9 +1104,7 @@ function tuiReducer(state, action) {
1061
1104
  ...state,
1062
1105
  agents: {
1063
1106
  ...state.agents,
1064
- [action.agentId]: { ...agent, logs: appendLog(agent.logs, `
1065
- ${action.text}
1066
- `) }
1107
+ [action.agentId]: { ...agent, logs: appendLog(agent.logs, action.text, "system") }
1067
1108
  }
1068
1109
  };
1069
1110
  }
@@ -1457,10 +1498,10 @@ var HELP_GROUPS = [
1457
1498
  }
1458
1499
  ];
1459
1500
  var KEY_COLUMN = 16;
1460
- var TURN_ROWS = 3;
1461
- var HISTORY_CHROME_ROWS = 4;
1462
- function historyCapacity(maxRows) {
1463
- return Math.max(1, Math.floor((maxRows - HISTORY_CHROME_ROWS) / TURN_ROWS));
1501
+ var ITEM_ROWS = 3;
1502
+ var OVERLAY_CHROME_ROWS = 4;
1503
+ function overlayCapacity(maxRows) {
1504
+ return Math.max(1, Math.floor((maxRows - OVERLAY_CHROME_ROWS) / ITEM_ROWS));
1464
1505
  }
1465
1506
  function listWindowStart(selected, total, capacity) {
1466
1507
  const maxStart = Math.max(0, total - capacity);
@@ -1486,81 +1527,6 @@ function HelpOverlay({ width, maxRows }) {
1486
1527
  /* @__PURE__ */ jsx2(Box2, { marginTop: gap, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "esc or ? to close" }) })
1487
1528
  ] });
1488
1529
  }
1489
- function outcome(turn) {
1490
- if (turn.reply) {
1491
- return turn.reply.mode === "ask" ? { text: `${GLYPH.ask} needs input`, color: COLOR.warn } : { text: `${STATUS.success.glyph} answered`, color: COLOR.ok };
1492
- }
1493
- if (turn.interrupted || turn.phase !== "complete") {
1494
- return { text: "interrupted", color: COLOR.warn };
1495
- }
1496
- if (turn.acceptance) {
1497
- switch (turn.acceptance.status) {
1498
- case "approved":
1499
- return { text: `${STATUS.success.glyph} approved`, color: COLOR.ok };
1500
- case "changes_required":
1501
- return { text: `${GLYPH.warn} changes required`, color: COLOR.warn };
1502
- case "failed":
1503
- return { text: `${STATUS.failed.glyph} acceptance failed`, color: COLOR.error };
1504
- case "cancelled":
1505
- return { text: `${STATUS.cancelled.glyph} acceptance cancelled`, color: COLOR.warn };
1506
- }
1507
- }
1508
- if (turn.cancelled) return { text: `${STATUS.cancelled.glyph} cancelled`, color: COLOR.warn };
1509
- return turn.success ? { text: `${STATUS.success.glyph} done`, color: COLOR.ok } : { text: `${STATUS.failed.glyph} failed`, color: COLOR.error };
1510
- }
1511
- function HistoryOverlay({
1512
- turns,
1513
- selected,
1514
- width,
1515
- maxRows
1516
- }) {
1517
- const capacity = historyCapacity(maxRows);
1518
- const start = listWindowStart(selected, turns.length, capacity);
1519
- const visible = turns.slice(start, start + capacity);
1520
- const range = turns.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${turns.length}` : "";
1521
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1522
- /* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
1523
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "HISTORY" }),
1524
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1525
- turns.length,
1526
- " earlier"
1527
- ] })
1528
- ] }),
1529
- turns.length === 0 ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No earlier turns yet." }) }) : /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: visible.map((turn, offset) => {
1530
- const index = start + offset;
1531
- const focused = index === selected;
1532
- const state = outcome(turn);
1533
- const { done, total } = progress(turn);
1534
- const time = turn.totalDuration === void 0 ? "" : ` \xB7 ${formatDuration(turn.totalDuration)}`;
1535
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [
1536
- /* @__PURE__ */ jsxs2(Box2, { children: [
1537
- /* @__PURE__ */ jsx2(Text2, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
1538
- /* @__PURE__ */ jsxs2(Text2, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
1539
- "turn ",
1540
- turn.turn
1541
- ] }),
1542
- /* @__PURE__ */ jsxs2(Text2, { color: state.color, children: [
1543
- " \xB7 ",
1544
- state.text
1545
- ] }),
1546
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1547
- time,
1548
- " \xB7 ",
1549
- done,
1550
- "/",
1551
- total,
1552
- " agents"
1553
- ] })
1554
- ] }),
1555
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate-end", children: [
1556
- " ",
1557
- truncate(turn.requirement ?? "(no requirement)", Math.max(8, width - 6))
1558
- ] })
1559
- ] }, turn.turn);
1560
- }) }),
1561
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: `\u2191\u2193 select \xB7 enter open \xB7 c continue \xB7 esc close${range}` }) })
1562
- ] });
1563
- }
1564
1530
  function SessionsOverlay({
1565
1531
  sessions,
1566
1532
  selected,
@@ -1569,7 +1535,7 @@ function SessionsOverlay({
1569
1535
  maxRows,
1570
1536
  now
1571
1537
  }) {
1572
- const capacity = historyCapacity(maxRows);
1538
+ const capacity = overlayCapacity(maxRows);
1573
1539
  const start = listWindowStart(selected, sessions.length, capacity);
1574
1540
  const visible = sessions.slice(start, start + capacity);
1575
1541
  const range = sessions.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${sessions.length}` : "";
@@ -1990,7 +1956,7 @@ function Prompt({
1990
1956
  const caret = line.caret;
1991
1957
  const rest = caret === void 0 ? "" : line.text.slice(caret);
1992
1958
  const cell = [...ghost ? hint : rest][0] ?? " ";
1993
- const tail2 = ghost ? [...hint].slice(1).join("") : rest.slice(cell.length);
1959
+ const tail = ghost ? [...hint].slice(1).join("") : rest.slice(cell.length);
1994
1960
  return (
1995
1961
  // biome-ignore lint/suspicious/noArrayIndexKey: a text row has no identity beyond its position — the whole prompt is rebuilt on every keystroke anyway
1996
1962
  /* @__PURE__ */ jsxs5(Box5, { flexShrink: 0, children: [
@@ -1998,7 +1964,7 @@ function Prompt({
1998
1964
  caret === void 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: line.text }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1999
1965
  /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: line.text.slice(0, caret) }),
2000
1966
  /* @__PURE__ */ jsx5(Text5, { inverse: caretVisible, dimColor: !caretVisible && ghost, children: cell }),
2001
- /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: tail2 })
1967
+ /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: tail })
2002
1968
  ] })
2003
1969
  ] }, start + index)
2004
1970
  );
@@ -2207,7 +2173,8 @@ function formatTurnMarkdown(turn) {
2207
2173
  lines.push("");
2208
2174
  lines.push("## Logs", "");
2209
2175
  for (const agent of agents) {
2210
- lines.push(`### ${agent.id}: ${agent.name}`, "", "```", agent.logs ?? "", "```", "");
2176
+ const logsText = agent.logs.map((s) => s.text).join("");
2177
+ lines.push(`### ${agent.id}: ${agent.name}`, "", "```", logsText ?? "", "```", "");
2211
2178
  }
2212
2179
  }
2213
2180
  return `${lines.join("\n").trimEnd()}
@@ -2237,7 +2204,7 @@ var STATUSES = /* @__PURE__ */ new Set([
2237
2204
  ]);
2238
2205
  function isAgentView(value) {
2239
2206
  if (!isObject2(value)) return false;
2240
- return typeof value.id === "string" && typeof value.name === "string" && typeof value.logs === "string" && typeof value.status === "string" && STATUSES.has(value.status);
2207
+ return typeof value.id === "string" && typeof value.name === "string" && Array.isArray(value.logs) && typeof value.status === "string" && STATUSES.has(value.status);
2241
2208
  }
2242
2209
  function isJournalTurn(value) {
2243
2210
  if (!isObject2(value)) return false;
@@ -2265,13 +2232,27 @@ function saveJournal(workDir, sessionId, journal) {
2265
2232
  if (isEmpty(journal) && !fs4.existsSync(target)) return Promise.resolve();
2266
2233
  return writeJsonFile(target, JSON.stringify(journal));
2267
2234
  }
2268
- function tail(text, max) {
2269
- return text.length > max ? `\u2026${text.slice(text.length - max)}` : text;
2235
+ function tailSegments(segs, max) {
2236
+ let total = segs.reduce((sum, s) => sum + s.text.length, 0);
2237
+ if (total <= max) return segs;
2238
+ const result = [...segs];
2239
+ while (total > max && result.length > 0) {
2240
+ const first = result[0];
2241
+ const excess = total - max;
2242
+ if (first.text.length <= excess) {
2243
+ total -= first.text.length;
2244
+ result.shift();
2245
+ } else {
2246
+ result[0] = { ...first, text: `\u2026${first.text.slice(excess)}` };
2247
+ break;
2248
+ }
2249
+ }
2250
+ return result;
2270
2251
  }
2271
2252
  function journalTurn(state, pending) {
2272
2253
  const agents = {};
2273
2254
  for (const [id, agent] of Object.entries(state.agents)) {
2274
- agents[id] = { ...agent, logs: tail(agent.logs, MAX_JOURNAL_LOG_CHARS) };
2255
+ agents[id] = { ...agent, logs: tailSegments(agent.logs, MAX_JOURNAL_LOG_CHARS) };
2275
2256
  }
2276
2257
  return {
2277
2258
  turn: state.turn,
@@ -2422,10 +2403,10 @@ function StatusLine({
2422
2403
  }
2423
2404
  if (state.phase === "complete") {
2424
2405
  const time = state.totalDuration === void 0 ? "" : ` ${formatDuration(state.totalDuration)}`;
2425
- const outcome2 = state.acceptance ? `${state.success ? STATUS.success.glyph : STATUS.failed.glyph} ${state.acceptance.status === "approved" ? "delivery approved" : state.acceptance.status === "changes_required" ? "changes required" : "acceptance failed"}${time}` : state.success ? `${STATUS.success.glyph} done${time}` : `${STATUS.failed.glyph} finished with failures${time}`;
2406
+ const outcome = state.acceptance ? `${state.success ? STATUS.success.glyph : STATUS.failed.glyph} ${state.acceptance.status === "approved" ? "delivery approved" : state.acceptance.status === "changes_required" ? "changes required" : "acceptance failed"}${time}` : state.success ? `${STATUS.success.glyph} done${time}` : `${STATUS.failed.glyph} finished with failures${time}`;
2426
2407
  const cost = state.totalCostUsd === void 0 ? "" : ` \xB7 ${formatCost(state.totalCostUsd)}`;
2427
2408
  return /* @__PURE__ */ jsxs6(Text6, { color: state.success ? COLOR.ok : COLOR.error, wrap: "truncate-end", children: [
2428
- outcome2,
2409
+ outcome,
2429
2410
  cost,
2430
2411
  state.warning ? ` \xB7 ${GLYPH.warn} ${state.warning}` : ""
2431
2412
  ] });
@@ -2493,8 +2474,6 @@ function App({
2493
2474
  launchNoticeRef.current ?? restored.notice
2494
2475
  );
2495
2476
  const [helpOpen, setHelpOpen] = useState(false);
2496
- const [historyOpen, setHistoryOpen] = useState(false);
2497
- const [historyCursor, setHistoryCursor] = useState(0);
2498
2477
  const [sessionsOpen, setSessionsOpen] = useState(false);
2499
2478
  const [sessionCursor, setSessionCursor] = useState(0);
2500
2479
  const [sessionList, setSessionList] = useState([]);
@@ -2506,6 +2485,14 @@ function App({
2506
2485
  const sessionsRef = useRef(null);
2507
2486
  if (!sessionsRef.current) sessionsRef.current = providedSessions ?? new AgentSessionManager();
2508
2487
  const continuationRef = useRef(null);
2488
+ useEffect(() => {
2489
+ if (initialSession?.kind !== "resume" || !("id" in initialSession)) return;
2490
+ const history = restoredRef.current?.history ?? [];
2491
+ const last = history[history.length - 1];
2492
+ if (last) dispatch({ type: "view:open", turn: last.turn });
2493
+ const resumable = [...history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
2494
+ continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
2495
+ }, [initialSession]);
2509
2496
  const activeTurnRef = useRef(null);
2510
2497
  const reporterErrorRef = useRef(null);
2511
2498
  const closingRef = useRef(false);
@@ -2815,6 +2802,10 @@ function App({
2815
2802
  leaderIds: leaderAgentIds(config),
2816
2803
  history: restored2.history
2817
2804
  });
2805
+ const lastTurn = restored2.history[restored2.history.length - 1];
2806
+ if (lastTurn) {
2807
+ dispatch({ type: "view:open", turn: lastTurn.turn });
2808
+ }
2818
2809
  setQueue(restored2.queue);
2819
2810
  queuePausedRef.current = restored2.queue.length > 0;
2820
2811
  setQueuePaused(restored2.queue.length > 0);
@@ -2899,8 +2890,6 @@ function App({
2899
2890
  () => query === void 0 ? [] : matchCommands(query, running),
2900
2891
  [query, running]
2901
2892
  );
2902
- const historyTurns = useMemo(() => [...state.history].reverse(), [state.history]);
2903
- const historyIndex = historyTurns.length === 0 ? 0 : Math.min(historyCursor, historyTurns.length - 1);
2904
2893
  const shown = state.view ?? state;
2905
2894
  const arg = argQuery(line.text);
2906
2895
  const argRows = arg ? completeAgents(arg.partial, Object.keys(shown.agents)).map((id) => ({
@@ -2915,11 +2904,11 @@ function App({
2915
2904
  const paletteTotal = allRows.length;
2916
2905
  const menuIndex = menuCursor.query === paletteKey ? menuCursor.index : 0;
2917
2906
  const selected = paletteTotal === 0 ? 0 : Math.min(menuIndex, paletteTotal - 1);
2918
- const menuOpen = !helpOpen && !historyOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
2919
- const argOpen = !helpOpen && !historyOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
2907
+ const menuOpen = !helpOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
2908
+ const argOpen = !helpOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
2920
2909
  const paletteOpen = menuOpen || argOpen;
2921
2910
  const front = interactions[0];
2922
- const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: void 0, released: false };
2911
+ const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: 0, released: false };
2923
2912
  const frontQuestion = front?.kind === "question" ? front : void 0;
2924
2913
  const questionFields = useMemo(
2925
2914
  () => frontQuestion ? formFields(frontQuestion.request) : [],
@@ -2937,7 +2926,7 @@ function App({
2937
2926
  columns,
2938
2927
  rows,
2939
2928
  turn: state.turn,
2940
- overlay: helpOpen || historyOpen || sessionsOpen,
2929
+ overlay: helpOpen || sessionsOpen,
2941
2930
  menuRows: paletteOpen ? paletteTotal : 0,
2942
2931
  queueRows: queue.length,
2943
2932
  interactionRows: interactionRows(interactions, Math.max(MIN_COLUMNS, columns)),
@@ -3181,15 +3170,6 @@ function App({
3181
3170
  dispatch({ type: "stream:clear" });
3182
3171
  setNotice(`cleared ${shown2.focusId}`);
3183
3172
  return;
3184
- case "history":
3185
- if (state.history.length === 0) {
3186
- setNotice("no earlier turns yet");
3187
- return;
3188
- }
3189
- setNotice(void 0);
3190
- setHistoryCursor(0);
3191
- setHistoryOpen(true);
3192
- return;
3193
3173
  case "sessions": {
3194
3174
  const list = listSessions(config.workDir);
3195
3175
  setSessionList(list);
@@ -3395,8 +3375,6 @@ function App({
3395
3375
  if (key.ctrl && char === "c") {
3396
3376
  if (helpOpen) {
3397
3377
  setHelpOpen(false);
3398
- } else if (historyOpen) {
3399
- setHistoryOpen(false);
3400
3378
  } else if (sessionsOpen) {
3401
3379
  setSessionsOpen(false);
3402
3380
  } else if (abortRef.current) {
@@ -3416,43 +3394,6 @@ function App({
3416
3394
  if (key.escape || char === "?") setHelpOpen(false);
3417
3395
  return;
3418
3396
  }
3419
- if (historyOpen) {
3420
- if (key.escape) {
3421
- setHistoryOpen(false);
3422
- return;
3423
- }
3424
- if (historyTurns.length === 0) return;
3425
- if (key.upArrow) {
3426
- setHistoryCursor((historyIndex - 1 + historyTurns.length) % historyTurns.length);
3427
- return;
3428
- }
3429
- if (key.downArrow) {
3430
- setHistoryCursor((historyIndex + 1) % historyTurns.length);
3431
- return;
3432
- }
3433
- if (key.return) {
3434
- dispatch({ type: "view:open", turn: historyTurns[historyIndex].turn });
3435
- setHistoryOpen(false);
3436
- return;
3437
- }
3438
- if (char === "c") {
3439
- const target = historyTurns[historyIndex];
3440
- const sessions = turnSessions(target);
3441
- const count = Object.keys(sessions).length;
3442
- if (count === 0) {
3443
- setNotice(`turn ${target.turn} has no agent sessions to continue`);
3444
- return;
3445
- }
3446
- continuationRef.current = { turn: target.turn, sessions };
3447
- dispatch({ type: "view:open", turn: target.turn });
3448
- setHistoryOpen(false);
3449
- setNotice(
3450
- `continuing turn ${target.turn} \u2014 ${count} ${count === 1 ? "session" : "sessions"} \xB7 type your next instruction`
3451
- );
3452
- return;
3453
- }
3454
- return;
3455
- }
3456
3397
  if (sessionsOpen) {
3457
3398
  if (key.escape) {
3458
3399
  setSessionsOpen(false);
@@ -3743,7 +3684,7 @@ function App({
3743
3684
  const handlerRef = useRef(handleKey);
3744
3685
  handlerRef.current = handleKey;
3745
3686
  useInput((char, key) => handlerRef.current(char, key));
3746
- const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \xB7 esc close" : sessionsOpen ? "\u2191\u2193 select \xB7 enter open \xB7 d delete \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 tab complete \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 tab complete \xB7 enter run \xB7 esc close" : running ? checkpointResolveRef.current ? `/continue \xB7 /modify <instruction> \xB7 /cancel` : interactions.length > 0 ? layout.interactionRows === 0 ? (
3687
+ const keys = helpOpen ? "esc close" : sessionsOpen ? "\u2191\u2193 select \xB7 enter open \xB7 d delete \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 tab complete \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 tab complete \xB7 enter run \xB7 esc close" : running ? checkpointResolveRef.current ? `/continue \xB7 /modify <instruction> \xB7 /cancel` : interactions.length > 0 ? layout.interactionRows === 0 ? (
3747
3688
  // Too short to dock the bar, so the commands are the only way in.
3748
3689
  "decide with /approve 1 \xB7 /deny \xB7 /respond"
3749
3690
  ) : interactions[0].kind === "question" ? "tab field \xB7 \u2191\u2193 pick \xB7 enter next \xB7 /dismiss" : menu.released ? "/approve 1 and /deny still reach it" : "\u2191\u2193 or 1-9 choose \xB7 enter confirm \xB7 esc to type" : "enter queues \xB7 ctrl+c cancel" : state.view ? "esc live \xB7 tab agent \xB7 ? help" : state.reply ? "enter sends \xB7 / commands \xB7 ? help" : "tab switch \xB7 / commands \xB7 ? help";
@@ -3773,15 +3714,7 @@ function App({
3773
3714
  flexShrink: 0,
3774
3715
  flexDirection: "column",
3775
3716
  overflow: "hidden",
3776
- children: helpOpen ? /* @__PURE__ */ jsx6(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : historyOpen ? /* @__PURE__ */ jsx6(
3777
- HistoryOverlay,
3778
- {
3779
- turns: historyTurns,
3780
- selected: historyIndex,
3781
- width: layout.columns,
3782
- maxRows: layout.bodyHeight
3783
- }
3784
- ) : sessionsOpen ? /* @__PURE__ */ jsx6(
3717
+ children: helpOpen ? /* @__PURE__ */ jsx6(HelpOverlay, { width: layout.columns, maxRows: layout.bodyHeight }) : sessionsOpen ? /* @__PURE__ */ jsx6(
3785
3718
  SessionsOverlay,
3786
3719
  {
3787
3720
  sessions: sessionList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiclawcli/dashboard",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tui": {
@@ -25,8 +25,9 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@multiclawcli/core": "0.10.1",
28
+ "@multiclawcli/core": "0.12.0",
29
29
  "ink": "^8.0.0",
30
+ "md4x": "^0.0.30",
30
31
  "react": "^19.3.1",
31
32
  "string-width": "^8.3.0"
32
33
  },