@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.
- package/dist/tui/components/overlays.d.ts +5 -26
- package/dist/tui/format.d.ts +11 -0
- package/dist/tui/state.d.ts +7 -2
- package/dist/tui.js +115 -182
- package/package.json +3 -2
|
@@ -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
|
|
20
|
-
export declare function
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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[];
|
package/dist/tui/format.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/tui/state.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
655
|
-
|
|
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:
|
|
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
|
|
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
|
|
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, "
|
|
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 ===
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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, {
|
|
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
|
-
|
|
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:
|
|
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
|
|
1461
|
-
var
|
|
1462
|
-
function
|
|
1463
|
-
return Math.max(1, Math.floor((maxRows -
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
|
|
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" &&
|
|
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
|
|
2269
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 && !
|
|
2919
|
-
const argOpen = !helpOpen && !
|
|
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:
|
|
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 ||
|
|
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" :
|
|
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 }) :
|
|
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.
|
|
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.
|
|
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
|
},
|