@multiclawcli/dashboard 0.10.0 → 0.11.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,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
@@ -260,7 +260,35 @@ function resolveAgentId(input, ids) {
260
260
  import { Box, Text } from "ink";
261
261
 
262
262
  // src/tui/format.ts
263
+ import { renderToAnsi } from "md4x/napi";
263
264
  import stringWidth from "string-width";
265
+ var DIM = "\x1B[2m";
266
+ var CYAN = "\x1B[36m";
267
+ var YELLOW = "\x1B[33m";
268
+ var RESET = "\x1B[0m";
269
+ function renderSegments(segments, _width) {
270
+ const parts = [];
271
+ let textBuf = "";
272
+ const flushText = () => {
273
+ if (!textBuf) return;
274
+ parts.push(renderToAnsi(textBuf, { heal: true }));
275
+ textBuf = "";
276
+ };
277
+ for (const seg of segments) {
278
+ if (seg.kind === "text") {
279
+ textBuf += seg.text;
280
+ } else {
281
+ flushText();
282
+ if (seg.kind === "tool") {
283
+ parts.push(`${DIM}${CYAN}\u25B8 ${seg.text}${RESET}`);
284
+ } else if (seg.kind === "system") {
285
+ parts.push(`${DIM}${YELLOW}${seg.text}${RESET}`);
286
+ }
287
+ }
288
+ }
289
+ flushText();
290
+ return parts.join("\n");
291
+ }
264
292
  function truncate(text, max) {
265
293
  if (max <= 0) return "";
266
294
  if (text.length <= max) return text;
@@ -650,12 +678,29 @@ var initialState = {
650
678
  function stripAnsi(text) {
651
679
  return text.replace(ANSI_PATTERN, "");
652
680
  }
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;
681
+ function appendLog(previous, chunk, kind = "text") {
682
+ const text = stripAnsi(chunk);
683
+ if (!text) return previous;
684
+ const last = previous[previous.length - 1];
685
+ const next = last?.kind === kind ? [...previous.slice(0, -1), { kind, text: last.text + text }] : [...previous, { kind, text }];
686
+ let total = next.reduce((sum, s) => sum + s.text.length, 0);
687
+ if (total <= MAX_LOG_CHARS) return next;
688
+ const result = [...next];
689
+ while (total > MAX_LOG_CHARS && result.length > 0) {
690
+ const first = result[0];
691
+ const excess = total - MAX_LOG_CHARS;
692
+ if (first.text.length <= excess) {
693
+ total -= first.text.length;
694
+ result.shift();
695
+ } else {
696
+ result[0] = { ...first, text: first.text.slice(excess) };
697
+ break;
698
+ }
699
+ }
700
+ return result;
656
701
  }
657
702
  function blankAgent(id, name, patch = {}) {
658
- return { id, name, status: "pending", logs: "", ...patch };
703
+ return { id, name, status: "pending", logs: [], ...patch };
659
704
  }
660
705
  function epoch(iso) {
661
706
  const value = Date.parse(iso);
@@ -787,7 +832,7 @@ function reduceEvent(state, event) {
787
832
  error: void 0,
788
833
  usage: void 0,
789
834
  costUsd: void 0,
790
- logs: existing.startedAt !== void 0 && existing.logs ? appendLog(existing.logs, "\n[agent restarted]\n") : existing.logs
835
+ logs: existing.startedAt !== void 0 && existing.logs.length > 0 ? appendLog(existing.logs, "[agent restarted]", "system") : existing.logs
791
836
  } : blankAgent(agentId, agentName, {
792
837
  icon,
793
838
  model,
@@ -813,7 +858,7 @@ function reduceEvent(state, event) {
813
858
  ...state,
814
859
  agents: {
815
860
  ...state.agents,
816
- [agentId]: { ...existing, logs: appendLog(existing.logs, chunk) }
861
+ [agentId]: { ...existing, logs: appendLog(existing.logs, chunk, kind ?? "text") }
817
862
  }
818
863
  };
819
864
  }
@@ -835,11 +880,10 @@ function reduceEvent(state, event) {
835
880
  const existing = state.agents[result.agentId];
836
881
  const next = existing ? { ...existing, ...patch } : blankAgent(result.agentId, result.agentName, patch);
837
882
  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
- `);
883
+ if (next.logs.length === 0 && result.output)
884
+ next.logs = appendLog([], result.output, "text");
885
+ if (result.error && !next.logs.some((s) => s.text.includes(stripAnsi(result.error)))) {
886
+ next.logs = appendLog(next.logs, result.error, "system");
843
887
  }
844
888
  }
845
889
  return { ...state, agents: { ...state.agents, [result.agentId]: next } };
@@ -852,12 +896,14 @@ function reduceEvent(state, event) {
852
896
  }
853
897
  case "agent:retrying": {
854
898
  const { agentId, agentName, attempt, maxAttempts, error } = event.payload;
855
- const note = `
856
- [retry ${attempt}/${maxAttempts}] ${error}
857
- `;
899
+ const note = `[retry ${attempt}/${maxAttempts}] ${error}`;
858
900
  const existing = state.agents[agentId];
859
901
  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 });
902
+ const next = existing ? { ...existing, status: "retrying", retry, logs: appendLog(existing.logs, note, "system") } : blankAgent(agentId, agentName, {
903
+ status: "retrying",
904
+ retry,
905
+ logs: appendLog([], note, "system")
906
+ });
861
907
  return { ...state, agents: { ...state.agents, [agentId]: next } };
862
908
  }
863
909
  case "orchestration:warning":
@@ -880,7 +926,9 @@ function reduceEvent(state, event) {
880
926
  }
881
927
  }
882
928
  function wrapLog(logs, width) {
883
- return logs.split("\n").flatMap((line) => wrapLine(line, width));
929
+ if (logs.length === 0) return [];
930
+ const rendered = renderSegments(logs, width);
931
+ return rendered.trimEnd().split("\n").flatMap((line) => wrapLine(line, width));
884
932
  }
885
933
  function focusedRowCount(turn, width) {
886
934
  const agent = turn.focusId ? turn.agents[turn.focusId] : void 0;
@@ -1052,7 +1100,7 @@ function tuiReducer(state, action) {
1052
1100
  const id = turn.focusId;
1053
1101
  const agent = id ? turn.agents[id] : void 0;
1054
1102
  if (!id || !agent) return turn;
1055
- return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: "" } }, scroll: 0 };
1103
+ return { ...turn, agents: { ...turn.agents, [id]: { ...agent, logs: [] } }, scroll: 0 };
1056
1104
  });
1057
1105
  case "stream:note": {
1058
1106
  const agent = state.agents[action.agentId];
@@ -1061,9 +1109,7 @@ function tuiReducer(state, action) {
1061
1109
  ...state,
1062
1110
  agents: {
1063
1111
  ...state.agents,
1064
- [action.agentId]: { ...agent, logs: appendLog(agent.logs, `
1065
- ${action.text}
1066
- `) }
1112
+ [action.agentId]: { ...agent, logs: appendLog(agent.logs, action.text, "system") }
1067
1113
  }
1068
1114
  };
1069
1115
  }
@@ -1990,7 +2036,7 @@ function Prompt({
1990
2036
  const caret = line.caret;
1991
2037
  const rest = caret === void 0 ? "" : line.text.slice(caret);
1992
2038
  const cell = [...ghost ? hint : rest][0] ?? " ";
1993
- const tail2 = ghost ? [...hint].slice(1).join("") : rest.slice(cell.length);
2039
+ const tail = ghost ? [...hint].slice(1).join("") : rest.slice(cell.length);
1994
2040
  return (
1995
2041
  // biome-ignore lint/suspicious/noArrayIndexKey: a text row has no identity beyond its position — the whole prompt is rebuilt on every keystroke anyway
1996
2042
  /* @__PURE__ */ jsxs5(Box5, { flexShrink: 0, children: [
@@ -1998,7 +2044,7 @@ function Prompt({
1998
2044
  caret === void 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: line.text }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1999
2045
  /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: line.text.slice(0, caret) }),
2000
2046
  /* @__PURE__ */ jsx5(Text5, { inverse: caretVisible, dimColor: !caretVisible && ghost, children: cell }),
2001
- /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: tail2 })
2047
+ /* @__PURE__ */ jsx5(Text5, { dimColor: ghost, children: tail })
2002
2048
  ] })
2003
2049
  ] }, start + index)
2004
2050
  );
@@ -2207,7 +2253,8 @@ function formatTurnMarkdown(turn) {
2207
2253
  lines.push("");
2208
2254
  lines.push("## Logs", "");
2209
2255
  for (const agent of agents) {
2210
- lines.push(`### ${agent.id}: ${agent.name}`, "", "```", agent.logs ?? "", "```", "");
2256
+ const logsText = agent.logs.map((s) => s.text).join("");
2257
+ lines.push(`### ${agent.id}: ${agent.name}`, "", "```", logsText ?? "", "```", "");
2211
2258
  }
2212
2259
  }
2213
2260
  return `${lines.join("\n").trimEnd()}
@@ -2237,7 +2284,7 @@ var STATUSES = /* @__PURE__ */ new Set([
2237
2284
  ]);
2238
2285
  function isAgentView(value) {
2239
2286
  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);
2287
+ return typeof value.id === "string" && typeof value.name === "string" && Array.isArray(value.logs) && typeof value.status === "string" && STATUSES.has(value.status);
2241
2288
  }
2242
2289
  function isJournalTurn(value) {
2243
2290
  if (!isObject2(value)) return false;
@@ -2265,13 +2312,27 @@ function saveJournal(workDir, sessionId, journal) {
2265
2312
  if (isEmpty(journal) && !fs4.existsSync(target)) return Promise.resolve();
2266
2313
  return writeJsonFile(target, JSON.stringify(journal));
2267
2314
  }
2268
- function tail(text, max) {
2269
- return text.length > max ? `\u2026${text.slice(text.length - max)}` : text;
2315
+ function tailSegments(segs, max) {
2316
+ let total = segs.reduce((sum, s) => sum + s.text.length, 0);
2317
+ if (total <= max) return segs;
2318
+ const result = [...segs];
2319
+ while (total > max && result.length > 0) {
2320
+ const first = result[0];
2321
+ const excess = total - max;
2322
+ if (first.text.length <= excess) {
2323
+ total -= first.text.length;
2324
+ result.shift();
2325
+ } else {
2326
+ result[0] = { ...first, text: `\u2026${first.text.slice(excess)}` };
2327
+ break;
2328
+ }
2329
+ }
2330
+ return result;
2270
2331
  }
2271
2332
  function journalTurn(state, pending) {
2272
2333
  const agents = {};
2273
2334
  for (const [id, agent] of Object.entries(state.agents)) {
2274
- agents[id] = { ...agent, logs: tail(agent.logs, MAX_JOURNAL_LOG_CHARS) };
2335
+ agents[id] = { ...agent, logs: tailSegments(agent.logs, MAX_JOURNAL_LOG_CHARS) };
2275
2336
  }
2276
2337
  return {
2277
2338
  turn: state.turn,
@@ -2919,7 +2980,7 @@ function App({
2919
2980
  const argOpen = !helpOpen && !historyOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
2920
2981
  const paletteOpen = menuOpen || argOpen;
2921
2982
  const front = interactions[0];
2922
- const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: void 0, released: false };
2983
+ const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: 0, released: false };
2923
2984
  const frontQuestion = front?.kind === "question" ? front : void 0;
2924
2985
  const questionFields = useMemo(
2925
2986
  () => frontQuestion ? formFields(frontQuestion.request) : [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiclawcli/dashboard",
3
- "version": "0.10.0",
3
+ "version": "0.11.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.0",
28
+ "@multiclawcli/core": "0.11.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
  },