@multiclawcli/dashboard 0.13.0 → 0.14.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/panels.d.ts +1 -1
- package/dist/tui/layout.d.ts +2 -0
- package/dist/tui/session.d.ts +4 -0
- package/dist/tui/state.d.ts +8 -1
- package/dist/tui.js +165 -43
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LeaderMessage } from "@multiclawcli/core";
|
|
2
2
|
import { type TuiState, type TurnView } from "../state";
|
|
3
3
|
/** The roster: one row per agent, with the focused one called out by a caret. */
|
|
4
4
|
export declare function TeamPanel({ turn, width, now, spinner, }: {
|
package/dist/tui/layout.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface LayoutInput {
|
|
|
72
72
|
promptRows?: number;
|
|
73
73
|
/** Whether a command result is waiting to be shown. */
|
|
74
74
|
notice: boolean;
|
|
75
|
+
/** A single row showing route and selection reason. */
|
|
76
|
+
details?: boolean;
|
|
75
77
|
/** Whether there is a leader or any stage to draw. */
|
|
76
78
|
hasRail: boolean;
|
|
77
79
|
/**
|
package/dist/tui/session.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export declare const SESSION_VERSION = 1;
|
|
|
24
24
|
export declare const MAX_JOURNAL_LOG_CHARS = 1200;
|
|
25
25
|
/** The turn that was in flight when the journal was last written. */
|
|
26
26
|
export interface JournalTurn {
|
|
27
|
+
work?: import("@multiclawcli/core").WorkContext;
|
|
28
|
+
routing?: import("@multiclawcli/core").RoutingInfo;
|
|
29
|
+
sessionEvents?: import("@multiclawcli/core").SessionEvent[];
|
|
30
|
+
agentResults?: import("@multiclawcli/core").AgentResult[];
|
|
27
31
|
turn: number;
|
|
28
32
|
requirement: string;
|
|
29
33
|
startedAt?: number;
|
package/dist/tui/state.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface AgentView {
|
|
|
33
33
|
max: number;
|
|
34
34
|
};
|
|
35
35
|
error?: string;
|
|
36
|
+
handoffReason?: string;
|
|
36
37
|
/** Context-window usage the runtime reported, when it reported any. */
|
|
37
38
|
usage?: AgentOutput["usage"];
|
|
38
39
|
/** USD cost the runtime reported, when the provider priced itself. */
|
|
@@ -42,7 +43,7 @@ export interface AgentView {
|
|
|
42
43
|
* `/retry` can ask the agent to continue where it left off instead of
|
|
43
44
|
* re-exploring from scratch.
|
|
44
45
|
*/
|
|
45
|
-
|
|
46
|
+
session?: import("@multiclawcli/core").SessionRecord;
|
|
46
47
|
logs: LogSegment[];
|
|
47
48
|
}
|
|
48
49
|
export type TuiPhase = "idle" | "running" | "complete";
|
|
@@ -60,6 +61,11 @@ export interface TuiStageInfo extends StageInfo {
|
|
|
60
61
|
* a `TurnView` so they can render one without knowing which it is.
|
|
61
62
|
*/
|
|
62
63
|
export interface TurnView {
|
|
64
|
+
work?: import("@multiclawcli/core").WorkContext;
|
|
65
|
+
routing?: import("@multiclawcli/core").RoutingInfo;
|
|
66
|
+
sessionEvents?: import("@multiclawcli/core").SessionEvent[];
|
|
67
|
+
agentResults?: import("@multiclawcli/core").AgentResult[];
|
|
68
|
+
effectiveAgentResults?: import("@multiclawcli/core").AgentResult[];
|
|
63
69
|
turn: number;
|
|
64
70
|
requirement?: string;
|
|
65
71
|
effectiveRequirement?: string;
|
|
@@ -296,6 +302,7 @@ export declare function recordedTurns(state: TuiState): TurnView[];
|
|
|
296
302
|
* restart.
|
|
297
303
|
*/
|
|
298
304
|
export declare function conversationTurns(state: TuiState): TurnRecord[];
|
|
305
|
+
export declare function latestWorkFromTurns(turns: readonly TurnView[]): import("@multiclawcli/core").WorkContext | undefined;
|
|
299
306
|
export declare function tuiReducer(state: TuiState, action: TuiAction): TuiState;
|
|
300
307
|
/**
|
|
301
308
|
* Roll a stage's member statuses up into one status for the pipeline bar.
|
package/dist/tui.js
CHANGED
|
@@ -408,7 +408,7 @@ function computeLayout(input) {
|
|
|
408
408
|
const showRail = !welcome && !input.overlay && rows >= RAIL_MIN_ROWS && input.hasRail;
|
|
409
409
|
const promptRows = Math.max(1, input.promptRows ?? 1);
|
|
410
410
|
const railRows = showRail ? Math.min(MAX_RAIL_ROWS, Math.max(1, measureRailRows(input.railCells, columns))) : 0;
|
|
411
|
-
const fixedRows = 4 + MIN_BODY_ROWS + promptRows + (input.notice ? 1 : 0) + (railRows > 0 ? railRows + 1 : 0);
|
|
411
|
+
const fixedRows = 4 + MIN_BODY_ROWS + promptRows + (input.details ? 1 : 0) + (input.notice ? 1 : 0) + (railRows > 0 ? railRows + 1 : 0);
|
|
412
412
|
const queueRows = Math.min(
|
|
413
413
|
Math.max(0, input.queueRows ?? 0),
|
|
414
414
|
MAX_QUEUE_ROWS,
|
|
@@ -423,7 +423,7 @@ function computeLayout(input) {
|
|
|
423
423
|
1 + // rule under the header
|
|
424
424
|
(railRows > 0 ? railRows + 1 : 0) + // pipeline rail plus the gap above it
|
|
425
425
|
1 + // gap before the body
|
|
426
|
-
(input.notice ? 1 : 0) + queueRows + menuRows + promptRows + 1;
|
|
426
|
+
(input.notice ? 1 : 0) + (input.details ? 1 : 0) + queueRows + menuRows + promptRows + 1;
|
|
427
427
|
const barRoom = Math.max(0, rows - chrome - MIN_BODY_ROWS - 1);
|
|
428
428
|
const interactionRows2 = barRoom < MIN_INTERACTION_ROWS ? 0 : Math.min(Math.max(0, input.interactionRows ?? 0), MAX_INTERACTION_ROWS, barRoom);
|
|
429
429
|
const reserved = chrome + (interactionRows2 > 0 ? interactionRows2 + 1 : 0);
|
|
@@ -463,6 +463,7 @@ var STATUS = {
|
|
|
463
463
|
pending: { glyph: "\u25CB", color: COLOR.muted, dim: true, label: "queued" },
|
|
464
464
|
running: { glyph: "\u25D0", color: COLOR.accent, label: "running" },
|
|
465
465
|
retrying: { glyph: "\u21BB", color: COLOR.warn, label: "retrying" },
|
|
466
|
+
handed_back: { glyph: "\u21A9", color: COLOR.warn, label: "handed back" },
|
|
466
467
|
success: { glyph: "\u2713", color: COLOR.ok, label: "done" },
|
|
467
468
|
failed: { glyph: "\u2717", color: COLOR.error, label: "failed" },
|
|
468
469
|
// Stopped, not broken: a filled square reads as "halted" and shares the muted
|
|
@@ -812,11 +813,55 @@ function reduceEvent(state, event) {
|
|
|
812
813
|
}
|
|
813
814
|
};
|
|
814
815
|
}
|
|
816
|
+
case "agent:session": {
|
|
817
|
+
const { record, action } = event.payload;
|
|
818
|
+
const existing = state.agents[record.agentId];
|
|
819
|
+
const isCurrentWork = record.workId ? state.work?.id === record.workId : state.leaderIds.includes(record.agentId);
|
|
820
|
+
const startsBinding = action === "opening" || action === "load_requested";
|
|
821
|
+
const matchesBinding = !existing?.session || existing.session.bindingId === record.bindingId || startsBinding;
|
|
822
|
+
const history = state.history.map((turn) => {
|
|
823
|
+
if (!record.workId || turn.work?.id !== record.workId) return turn;
|
|
824
|
+
const member = turn.work.agents.find((item) => item.agentId === record.agentId);
|
|
825
|
+
if (member?.session?.bindingId !== record.bindingId) return turn;
|
|
826
|
+
return {
|
|
827
|
+
...turn,
|
|
828
|
+
agents: turn.agents[record.agentId]?.session?.bindingId === record.bindingId ? {
|
|
829
|
+
...turn.agents,
|
|
830
|
+
[record.agentId]: { ...turn.agents[record.agentId], session: record }
|
|
831
|
+
} : turn.agents,
|
|
832
|
+
work: {
|
|
833
|
+
...turn.work,
|
|
834
|
+
agents: turn.work.agents.map(
|
|
835
|
+
(item) => item.agentId === record.agentId ? { ...item, session: record } : item
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
const updatesCurrent = isCurrentWork && matchesBinding && existing;
|
|
841
|
+
const work = updatesCurrent && state.work && state.work.id === record.workId ? {
|
|
842
|
+
...state.work,
|
|
843
|
+
agents: state.work.agents.map(
|
|
844
|
+
(item) => item.agentId === record.agentId ? {
|
|
845
|
+
...item,
|
|
846
|
+
session: record.sessionId || !item.session?.sessionId || record.failure?.code === "cleanup_failed" ? record : item.session
|
|
847
|
+
} : item
|
|
848
|
+
)
|
|
849
|
+
} : state.work;
|
|
850
|
+
return {
|
|
851
|
+
...state,
|
|
852
|
+
work,
|
|
853
|
+
history,
|
|
854
|
+
sessionEvents: [...state.sessionEvents ?? [], event.payload],
|
|
855
|
+
agents: updatesCurrent ? { ...state.agents, [record.agentId]: { ...existing, session: record } } : state.agents
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
case "agent:handed-back":
|
|
815
859
|
case "agent:complete":
|
|
816
860
|
case "agent:failed": {
|
|
817
861
|
const result = event.payload;
|
|
818
862
|
const patch = {
|
|
819
863
|
status: result.status,
|
|
864
|
+
handoffReason: result.handoffReason,
|
|
820
865
|
duration: result.duration,
|
|
821
866
|
attempts: result.attempts,
|
|
822
867
|
retry: void 0,
|
|
@@ -825,10 +870,12 @@ function reduceEvent(state, event) {
|
|
|
825
870
|
costUsd: result.costUsd,
|
|
826
871
|
// Present only on the success path; a failed agent has no session to
|
|
827
872
|
// continue, so `/retry` re-runs it cold.
|
|
828
|
-
|
|
873
|
+
session: result.session ?? state.agents[result.agentId]?.session
|
|
829
874
|
};
|
|
830
875
|
const existing = state.agents[result.agentId];
|
|
831
876
|
const next = existing ? { ...existing, ...patch } : blankAgent(result.agentId, result.agentName, patch);
|
|
877
|
+
if (event.type === "agent:handed-back")
|
|
878
|
+
next.logs = appendLog(next.logs, "[handed back] " + result.handoffReason, "system");
|
|
832
879
|
if (event.type === "agent:failed") {
|
|
833
880
|
if (next.logs.length === 0 && result.output)
|
|
834
881
|
next.logs = appendLog([], result.output, "text");
|
|
@@ -837,7 +884,11 @@ function reduceEvent(state, event) {
|
|
|
837
884
|
next.logs = appendLog(next.logs, error, "system");
|
|
838
885
|
}
|
|
839
886
|
}
|
|
840
|
-
return {
|
|
887
|
+
return {
|
|
888
|
+
...state,
|
|
889
|
+
agentResults: [...state.agentResults ?? [], result],
|
|
890
|
+
agents: { ...state.agents, [result.agentId]: next }
|
|
891
|
+
};
|
|
841
892
|
}
|
|
842
893
|
case "agent:skipped": {
|
|
843
894
|
const { agentId, agentName, icon } = event.payload;
|
|
@@ -857,6 +908,15 @@ function reduceEvent(state, event) {
|
|
|
857
908
|
});
|
|
858
909
|
return { ...state, agents: { ...state.agents, [agentId]: next } };
|
|
859
910
|
}
|
|
911
|
+
case "orchestration:route":
|
|
912
|
+
return {
|
|
913
|
+
...state,
|
|
914
|
+
routing: event.payload.routing,
|
|
915
|
+
work: event.payload.work,
|
|
916
|
+
warning: event.payload.routing.reroutes ? "Worker handed work back \u2014 rerouting once" : state.warning
|
|
917
|
+
};
|
|
918
|
+
case "orchestration:routing-error":
|
|
919
|
+
return { ...state, warning: event.payload.message };
|
|
860
920
|
case "orchestration:warning":
|
|
861
921
|
return { ...state, warning: event.payload.message };
|
|
862
922
|
case "orchestration:complete":
|
|
@@ -864,6 +924,10 @@ function reduceEvent(state, event) {
|
|
|
864
924
|
...state,
|
|
865
925
|
phase: "complete",
|
|
866
926
|
success: event.payload.success,
|
|
927
|
+
work: event.payload.work,
|
|
928
|
+
routing: event.payload.routing,
|
|
929
|
+
agentResults: event.payload.agentResults,
|
|
930
|
+
effectiveAgentResults: event.payload.effectiveAgentResults,
|
|
867
931
|
acceptance: event.payload.acceptance,
|
|
868
932
|
cancelled: event.payload.cancelled,
|
|
869
933
|
effectiveRequirement: event.payload.requirement ?? state.effectiveRequirement,
|
|
@@ -1008,6 +1072,11 @@ function snapshot(state) {
|
|
|
1008
1072
|
autoFollow: state.autoFollow,
|
|
1009
1073
|
scroll: state.scroll,
|
|
1010
1074
|
success: state.success,
|
|
1075
|
+
work: state.work,
|
|
1076
|
+
routing: state.routing,
|
|
1077
|
+
sessionEvents: state.sessionEvents,
|
|
1078
|
+
agentResults: state.agentResults,
|
|
1079
|
+
effectiveAgentResults: state.effectiveAgentResults,
|
|
1011
1080
|
acceptance: state.acceptance,
|
|
1012
1081
|
totalDuration: state.totalDuration,
|
|
1013
1082
|
totalCostUsd: state.totalCostUsd,
|
|
@@ -1024,12 +1093,18 @@ function conversationTurns(state) {
|
|
|
1024
1093
|
for (const turn of recordedTurns(state)) {
|
|
1025
1094
|
if (turn.phase !== "complete" || turn.cancelled || !turn.requirement) continue;
|
|
1026
1095
|
turns.push(
|
|
1027
|
-
turn.reply ? { requirement: turn.requirement, reply: turn.reply } : { requirement: turn.requirement, outcome: conversationOutcome(turn) }
|
|
1096
|
+
turn.reply ? { requirement: turn.requirement, reply: turn.reply } : { requirement: turn.requirement, outcome: conversationOutcome(turn), work: turn.work }
|
|
1028
1097
|
);
|
|
1029
1098
|
}
|
|
1030
1099
|
return turns;
|
|
1031
1100
|
}
|
|
1101
|
+
function latestWorkFromTurns(turns) {
|
|
1102
|
+
const latest = [...turns].reverse().find((turn) => turn.work || !turn.reply && turn.stages.length > 0);
|
|
1103
|
+
if (!latest?.work || latest.cancelled) return void 0;
|
|
1104
|
+
return { ...latest.work, outcome: conversationOutcome(latest) };
|
|
1105
|
+
}
|
|
1032
1106
|
function conversationOutcome(turn) {
|
|
1107
|
+
if (!turn.interrupted && !turn.cancelled && turn.work?.outcome) return turn.work.outcome;
|
|
1033
1108
|
const reviewer = turn.acceptance?.reviewer;
|
|
1034
1109
|
const isWorker = (id) => id !== reviewer && !turn.leaderIds.includes(id);
|
|
1035
1110
|
const workers = Object.values(turn.agents).filter((agent) => isWorker(agent.id));
|
|
@@ -1190,6 +1265,7 @@ function stageStatus(stage, agents) {
|
|
|
1190
1265
|
if (stage.outcome !== void 0) return stage.outcome;
|
|
1191
1266
|
const statuses = stage.agents.map((a) => agents[a.id]?.status ?? "pending");
|
|
1192
1267
|
if (statuses.some((s) => s === "failed")) return "failed";
|
|
1268
|
+
if (statuses.some((s) => s === "handed_back")) return "handed_back";
|
|
1193
1269
|
if (statuses.some((s) => s === "running" || s === "retrying")) return "running";
|
|
1194
1270
|
if (statuses.length > 0 && statuses.every((s) => s === "success")) return "success";
|
|
1195
1271
|
if (statuses.length > 0 && statuses.every((s) => s === "skipped")) return "skipped";
|
|
@@ -1197,6 +1273,7 @@ function stageStatus(stage, agents) {
|
|
|
1197
1273
|
return "pending";
|
|
1198
1274
|
}
|
|
1199
1275
|
var SETTLED = /* @__PURE__ */ new Set([
|
|
1276
|
+
"handed_back",
|
|
1200
1277
|
"success",
|
|
1201
1278
|
"failed",
|
|
1202
1279
|
"cancelled",
|
|
@@ -1715,6 +1792,7 @@ function detailLines(field, width) {
|
|
|
1715
1792
|
}
|
|
1716
1793
|
|
|
1717
1794
|
// src/tui/components/panels.tsx
|
|
1795
|
+
import { describeSession } from "@multiclawcli/core";
|
|
1718
1796
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
1719
1797
|
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1720
1798
|
function rightColumn(agent, now) {
|
|
@@ -1820,6 +1898,10 @@ function StreamPanel({
|
|
|
1820
1898
|
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
|
|
1821
1899
|
/* @__PURE__ */ jsx4(Box4, { flexShrink: 1, children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate-end", children: [
|
|
1822
1900
|
/* @__PURE__ */ jsx4(Text4, { bold: true, children: agent?.name ?? "\u2014" }),
|
|
1901
|
+
agent?.session ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1902
|
+
" \xB7 ",
|
|
1903
|
+
describeSession(agent.session)
|
|
1904
|
+
] }) : null,
|
|
1823
1905
|
agent?.model ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
1824
1906
|
" \xB7 ",
|
|
1825
1907
|
agent.model
|
|
@@ -2041,6 +2123,7 @@ function CommandMenu({ rows, selected }) {
|
|
|
2041
2123
|
// src/tui/history.ts
|
|
2042
2124
|
import * as fs3 from "fs";
|
|
2043
2125
|
import * as path3 from "path";
|
|
2126
|
+
import { describeSession as describeSession2, isSessionEvent, isSessionRecord, isWorkContext } from "@multiclawcli/core";
|
|
2044
2127
|
|
|
2045
2128
|
// src/tui/sessions.ts
|
|
2046
2129
|
import { randomUUID } from "crypto";
|
|
@@ -2139,14 +2222,24 @@ function isAcceptance(value) {
|
|
|
2139
2222
|
function isTurnView(value) {
|
|
2140
2223
|
if (!isObject(value)) return false;
|
|
2141
2224
|
const turn = value;
|
|
2142
|
-
return typeof turn.turn === "number" && typeof turn.phase === "string" && (turn.cancelled === void 0 || typeof turn.cancelled === "boolean") && (turn.interrupted === void 0 || typeof turn.interrupted === "boolean") && (turn.acceptance === void 0 || isAcceptance(turn.acceptance)) && (turn.effectiveRequirement === void 0 || typeof turn.effectiveRequirement === "string") && isObject(turn.agents) && Object.values(turn.agents).every(
|
|
2143
|
-
(
|
|
2144
|
-
|
|
2145
|
-
)) && (stage.
|
|
2225
|
+
return typeof turn.turn === "number" && typeof turn.phase === "string" && (turn.cancelled === void 0 || typeof turn.cancelled === "boolean") && (turn.interrupted === void 0 || typeof turn.interrupted === "boolean") && (turn.work === void 0 || isWorkContext(turn.work)) && (turn.sessionEvents === void 0 || Array.isArray(turn.sessionEvents) && turn.sessionEvents.every(isSessionEvent)) && (turn.acceptance === void 0 || isAcceptance(turn.acceptance)) && (turn.effectiveRequirement === void 0 || typeof turn.effectiveRequirement === "string") && isObject(turn.agents) && Object.values(turn.agents).every(
|
|
2226
|
+
(agent) => isObject(agent) && (agent.session === void 0 || isSessionRecord(agent.session))
|
|
2227
|
+
) && Array.isArray(turn.stages) && turn.stages.every(
|
|
2228
|
+
(stage) => isObject(stage) && (stage.outcome === void 0 || [
|
|
2229
|
+
"pending",
|
|
2230
|
+
"running",
|
|
2231
|
+
"retrying",
|
|
2232
|
+
"handed_back",
|
|
2233
|
+
"success",
|
|
2234
|
+
"failed",
|
|
2235
|
+
"cancelled",
|
|
2236
|
+
"skipped"
|
|
2237
|
+
].includes(stage.outcome)) && (stage.memberStatuses === void 0 || isObject(stage.memberStatuses) && Object.values(stage.memberStatuses).every(
|
|
2146
2238
|
(status) => typeof status === "string" && [
|
|
2147
2239
|
"pending",
|
|
2148
2240
|
"running",
|
|
2149
2241
|
"retrying",
|
|
2242
|
+
"handed_back",
|
|
2150
2243
|
"success",
|
|
2151
2244
|
"failed",
|
|
2152
2245
|
"cancelled",
|
|
@@ -2189,6 +2282,27 @@ async function exportTurn(turn, filePath) {
|
|
|
2189
2282
|
function formatTurnMarkdown(turn) {
|
|
2190
2283
|
const lines = [`# Turn ${turn.turn}`, ""];
|
|
2191
2284
|
if (turn.requirement) lines.push("## Requirement", "", turn.requirement, "");
|
|
2285
|
+
if (turn.effectiveRequirement && turn.effectiveRequirement !== turn.requirement)
|
|
2286
|
+
lines.push("## Complete requirement", "", turn.effectiveRequirement, "");
|
|
2287
|
+
if (turn.routing) {
|
|
2288
|
+
lines.push(
|
|
2289
|
+
"## Routing",
|
|
2290
|
+
"",
|
|
2291
|
+
"Type: " + turn.routing.mode,
|
|
2292
|
+
"Work: " + (turn.routing.workId ?? "none"),
|
|
2293
|
+
"Reroutes: " + turn.routing.reroutes,
|
|
2294
|
+
""
|
|
2295
|
+
);
|
|
2296
|
+
if (turn.routing.error) lines.push("Incomplete: " + turn.routing.error, "");
|
|
2297
|
+
}
|
|
2298
|
+
if (turn.agentResults?.length) {
|
|
2299
|
+
lines.push("## Execution attempts", "");
|
|
2300
|
+
for (const result of turn.agentResults)
|
|
2301
|
+
lines.push(
|
|
2302
|
+
"- " + result.agentId + ": " + result.status + (result.handoffReason ? " \u2014 " + result.handoffReason : "") + (result.error ? " \u2014 " + result.error : "")
|
|
2303
|
+
);
|
|
2304
|
+
lines.push("");
|
|
2305
|
+
}
|
|
2192
2306
|
if (turn.reply) {
|
|
2193
2307
|
lines.push("## Reply", "", `_${turn.reply.mode}_`, "", turn.reply.message, "");
|
|
2194
2308
|
} else {
|
|
@@ -2209,11 +2323,20 @@ function formatTurnMarkdown(turn) {
|
|
|
2209
2323
|
}
|
|
2210
2324
|
}
|
|
2211
2325
|
}
|
|
2326
|
+
if (turn.sessionEvents?.length) {
|
|
2327
|
+
lines.push("## Session lifecycle", "");
|
|
2328
|
+
for (const event of turn.sessionEvents)
|
|
2329
|
+
lines.push(
|
|
2330
|
+
"- " + event.record.agentId + ": " + event.action + (event.requestedSessionId ? " target=" + event.requestedSessionId : "") + " \u2014 " + describeSession2(event.record) + (event.reason ? " \u2014 " + event.reason : "")
|
|
2331
|
+
);
|
|
2332
|
+
lines.push("");
|
|
2333
|
+
}
|
|
2212
2334
|
const agents = Object.values(turn.agents);
|
|
2213
2335
|
if (agents.length > 0) {
|
|
2214
2336
|
lines.push("## Agents", "");
|
|
2215
2337
|
for (const agent of agents) {
|
|
2216
2338
|
lines.push(`- ${agent.id}: ${agent.name} \u2014 ${agent.status}`);
|
|
2339
|
+
if (agent.session) lines.push(" " + describeSession2(agent.session));
|
|
2217
2340
|
}
|
|
2218
2341
|
lines.push("");
|
|
2219
2342
|
lines.push("## Logs", "");
|
|
@@ -2229,6 +2352,7 @@ function formatTurnMarkdown(turn) {
|
|
|
2229
2352
|
// src/tui/session.ts
|
|
2230
2353
|
import * as fs4 from "fs";
|
|
2231
2354
|
import * as path4 from "path";
|
|
2355
|
+
import { isSessionEvent as isSessionEvent2, isSessionRecord as isSessionRecord2, isWorkContext as isWorkContext2 } from "@multiclawcli/core";
|
|
2232
2356
|
var SESSION_FILE = "session.json";
|
|
2233
2357
|
var SESSION_VERSION = 1;
|
|
2234
2358
|
var MAX_JOURNAL_LOG_CHARS = 1200;
|
|
@@ -2242,6 +2366,7 @@ var STATUSES = /* @__PURE__ */ new Set([
|
|
|
2242
2366
|
"pending",
|
|
2243
2367
|
"running",
|
|
2244
2368
|
"retrying",
|
|
2369
|
+
"handed_back",
|
|
2245
2370
|
"success",
|
|
2246
2371
|
"failed",
|
|
2247
2372
|
"cancelled",
|
|
@@ -2249,11 +2374,11 @@ var STATUSES = /* @__PURE__ */ new Set([
|
|
|
2249
2374
|
]);
|
|
2250
2375
|
function isAgentView(value) {
|
|
2251
2376
|
if (!isObject2(value)) return false;
|
|
2252
|
-
return typeof value.id === "string" && typeof value.name === "string" && Array.isArray(value.logs) && typeof value.status === "string" && STATUSES.has(value.status);
|
|
2377
|
+
return typeof value.id === "string" && typeof value.name === "string" && (value.session === void 0 || isSessionRecord2(value.session)) && Array.isArray(value.logs) && typeof value.status === "string" && STATUSES.has(value.status);
|
|
2253
2378
|
}
|
|
2254
2379
|
function isJournalTurn(value) {
|
|
2255
2380
|
if (!isObject2(value)) return false;
|
|
2256
|
-
return Number.isInteger(value.turn) && typeof value.requirement === "string" && typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && (value.startedAt === void 0 || typeof value.startedAt === "number") && (value.effectiveRequirement === void 0 || typeof value.effectiveRequirement === "string") && (value.reason === void 0 || typeof value.reason === "string") && (value.warning === void 0 || typeof value.warning === "string") && (value.focusId === void 0 || typeof value.focusId === "string") && isObject2(value.agents) && Object.values(value.agents).every(isAgentView) && Array.isArray(value.leaderIds) && value.leaderIds.every((id) => typeof id === "string") && Array.isArray(value.stages) && value.stages.every(isObject2) && (value.pending === void 0 || isObject2(value.pending) && Number.isInteger(value.pending.permissions) && Number.isInteger(value.pending.questions));
|
|
2381
|
+
return Number.isInteger(value.turn) && typeof value.requirement === "string" && typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && (value.work === void 0 || isWorkContext2(value.work)) && (value.sessionEvents === void 0 || Array.isArray(value.sessionEvents) && value.sessionEvents.every(isSessionEvent2)) && (value.startedAt === void 0 || typeof value.startedAt === "number") && (value.effectiveRequirement === void 0 || typeof value.effectiveRequirement === "string") && (value.reason === void 0 || typeof value.reason === "string") && (value.warning === void 0 || typeof value.warning === "string") && (value.focusId === void 0 || typeof value.focusId === "string") && isObject2(value.agents) && Object.values(value.agents).every(isAgentView) && Array.isArray(value.leaderIds) && value.leaderIds.every((id) => typeof id === "string") && Array.isArray(value.stages) && value.stages.every(isObject2) && (value.pending === void 0 || isObject2(value.pending) && Number.isInteger(value.pending.permissions) && Number.isInteger(value.pending.questions));
|
|
2257
2382
|
}
|
|
2258
2383
|
function loadJournal(workDir, sessionId) {
|
|
2259
2384
|
try {
|
|
@@ -2301,6 +2426,10 @@ function journalTurn(state, pending) {
|
|
|
2301
2426
|
}
|
|
2302
2427
|
return {
|
|
2303
2428
|
turn: state.turn,
|
|
2429
|
+
work: state.work,
|
|
2430
|
+
routing: state.routing,
|
|
2431
|
+
sessionEvents: state.sessionEvents,
|
|
2432
|
+
agentResults: state.agentResults,
|
|
2304
2433
|
requirement: state.requirement ?? "",
|
|
2305
2434
|
startedAt: state.startedAt,
|
|
2306
2435
|
effectiveRequirement: state.effectiveRequirement,
|
|
@@ -2318,10 +2447,24 @@ var UNSETTLED = /* @__PURE__ */ new Set(["pending", "running", "retrying"]);
|
|
|
2318
2447
|
function interruptedTurn(entry) {
|
|
2319
2448
|
const agents = {};
|
|
2320
2449
|
for (const [id, agent] of Object.entries(entry.agents)) {
|
|
2321
|
-
agents[id] =
|
|
2450
|
+
agents[id] = {
|
|
2451
|
+
...agent,
|
|
2452
|
+
...UNSETTLED.has(agent.status) ? { status: "cancelled" } : {},
|
|
2453
|
+
session: agent.session ? { ...agent.session, state: agent.session.state === "closed" ? "closed" : "unknown" } : void 0
|
|
2454
|
+
};
|
|
2322
2455
|
}
|
|
2323
2456
|
return {
|
|
2324
2457
|
turn: entry.turn,
|
|
2458
|
+
work: entry.work ? {
|
|
2459
|
+
...entry.work,
|
|
2460
|
+
agents: entry.work.agents.map((agent) => ({
|
|
2461
|
+
...agent,
|
|
2462
|
+
session: agent.session ? { ...agent.session, state: agent.session.state === "closed" ? "closed" : "unknown" } : void 0
|
|
2463
|
+
}))
|
|
2464
|
+
} : void 0,
|
|
2465
|
+
routing: entry.routing,
|
|
2466
|
+
sessionEvents: entry.sessionEvents,
|
|
2467
|
+
agentResults: entry.agentResults,
|
|
2325
2468
|
requirement: entry.requirement,
|
|
2326
2469
|
effectiveRequirement: entry.effectiveRequirement,
|
|
2327
2470
|
reason: entry.reason,
|
|
@@ -2407,20 +2550,6 @@ function pickTurn(state, raw, whenEmpty) {
|
|
|
2407
2550
|
if (!target.record.requirement) return { error: `turn ${target.record.turn} has no requirement` };
|
|
2408
2551
|
return { record: target.record, requirement: target.record.requirement };
|
|
2409
2552
|
}
|
|
2410
|
-
function turnSessions(turn) {
|
|
2411
|
-
const sessions = {};
|
|
2412
|
-
for (const agent of Object.values(turn.agents)) {
|
|
2413
|
-
if (agent.sessionId) sessions[agent.id] = agent.sessionId;
|
|
2414
|
-
}
|
|
2415
|
-
return sessions;
|
|
2416
|
-
}
|
|
2417
|
-
function continuationResume(continuation, live) {
|
|
2418
|
-
if (!continuation) return void 0;
|
|
2419
|
-
const sessions = Object.fromEntries(
|
|
2420
|
-
Object.entries(continuation.sessions).filter(([id]) => !live?.has(id))
|
|
2421
|
-
);
|
|
2422
|
-
return Object.keys(sessions).length > 0 ? { sessions } : void 0;
|
|
2423
|
-
}
|
|
2424
2553
|
function StatusLine({
|
|
2425
2554
|
state,
|
|
2426
2555
|
running,
|
|
@@ -2437,6 +2566,8 @@ function StatusLine({
|
|
|
2437
2566
|
return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, wrap: "truncate-end", children: state.warning });
|
|
2438
2567
|
if ((cancelled || state.cancelled) && !running)
|
|
2439
2568
|
return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, children: "cancelled \xB7 workspace changes are retained" });
|
|
2569
|
+
if (state.routing?.error && !running)
|
|
2570
|
+
return /* @__PURE__ */ jsx6(Text6, { color: COLOR.warn, wrap: "truncate-end", children: state.routing.error });
|
|
2440
2571
|
if (state.reply) {
|
|
2441
2572
|
const asking = state.reply.mode === "ask";
|
|
2442
2573
|
return /* @__PURE__ */ jsx6(Text6, { color: asking ? COLOR.warn : COLOR.ok, wrap: "truncate-end", children: asking ? `${GLYPH.ask} needs your input` : `${STATUS.success.glyph} answered` });
|
|
@@ -2548,13 +2679,6 @@ function App({
|
|
|
2548
2679
|
const abortRef = useRef(null);
|
|
2549
2680
|
const sessionsRef = useRef(null);
|
|
2550
2681
|
if (!sessionsRef.current) sessionsRef.current = providedSessions ?? new AgentSessionManager();
|
|
2551
|
-
const continuationRef = useRef(null);
|
|
2552
|
-
useEffect(() => {
|
|
2553
|
-
if (initialSession?.kind !== "resume" || !("id" in initialSession)) return;
|
|
2554
|
-
const history = restoredRef.current?.history ?? [];
|
|
2555
|
-
const resumable = [...history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
|
|
2556
|
-
continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
|
|
2557
|
-
}, [initialSession]);
|
|
2558
2682
|
const activeTurnRef = useRef(null);
|
|
2559
2683
|
const reporterErrorRef = useRef(null);
|
|
2560
2684
|
const closingRef = useRef(false);
|
|
@@ -2720,9 +2844,7 @@ function App({
|
|
|
2720
2844
|
if (closingRef.current || busyRef.current) return;
|
|
2721
2845
|
busyRef.current = true;
|
|
2722
2846
|
setRunning(true);
|
|
2723
|
-
const
|
|
2724
|
-
continuationRef.current = null;
|
|
2725
|
-
const resume = opts?.resume ?? continuationResume(continuation, sessionsRef.current);
|
|
2847
|
+
const resume = opts?.resume;
|
|
2726
2848
|
if (opts?.newSession) {
|
|
2727
2849
|
try {
|
|
2728
2850
|
await sessionsRef.current?.reset();
|
|
@@ -2779,6 +2901,7 @@ function App({
|
|
|
2779
2901
|
// An answer only reads as a requirement next to the question it answers,
|
|
2780
2902
|
// so the leader is handed the turns behind this one as well.
|
|
2781
2903
|
history: earlierTurns,
|
|
2904
|
+
latestWork: opts?.newSession ? void 0 : opts?.work ?? latestWorkFromTurns(recordedTurns(stateRef.current)),
|
|
2782
2905
|
logDir,
|
|
2783
2906
|
permissions: permissionMode === "ask" ? { mode: "ask", onRequest: requestPermission } : { mode: permissionMode },
|
|
2784
2907
|
// The console's mode is a live decision, so it outranks any per-agent
|
|
@@ -2879,8 +3002,6 @@ function App({
|
|
|
2879
3002
|
const restored2 = restoreConsole(config.workDir, id);
|
|
2880
3003
|
sessionIdRef.current = id;
|
|
2881
3004
|
persistedTurnRef.current = restored2.persistedTurn;
|
|
2882
|
-
const resumable = [...restored2.history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
|
|
2883
|
-
continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
|
|
2884
3005
|
dispatch({
|
|
2885
3006
|
type: "session:load",
|
|
2886
3007
|
agents: seedAgents(config),
|
|
@@ -2894,9 +3015,7 @@ function App({
|
|
|
2894
3015
|
setAwaitingAnswer(restored2.awaitingAnswer);
|
|
2895
3016
|
setError(void 0);
|
|
2896
3017
|
setCancelled(false);
|
|
2897
|
-
setNotice(
|
|
2898
|
-
restored2.notice ?? (resumable ? `switched session \xB7 continue with your next instruction` : `switched session \xB7 ${restored2.history.length} turn${restored2.history.length === 1 ? "" : "s"}`)
|
|
2899
|
-
);
|
|
3018
|
+
setNotice(restored2.notice ?? "switched session \xB7 route your next instruction");
|
|
2900
3019
|
},
|
|
2901
3020
|
[config]
|
|
2902
3021
|
);
|
|
@@ -3016,6 +3135,7 @@ function App({
|
|
|
3016
3135
|
queueRows: queue.length,
|
|
3017
3136
|
interactionRows: interactionRows(interactions, Math.max(MIN_COLUMNS, columns)),
|
|
3018
3137
|
promptRows,
|
|
3138
|
+
details: !!(state.reason || state.routing),
|
|
3019
3139
|
notice: notice !== void 0 || queuePaused && queue.length > 0,
|
|
3020
3140
|
hasRail: state.leaderIds.length > 0 || state.stages.length > 0,
|
|
3021
3141
|
railCells: railCellWidths(state)
|
|
@@ -3243,7 +3363,7 @@ function App({
|
|
|
3243
3363
|
return;
|
|
3244
3364
|
}
|
|
3245
3365
|
const sessions = Object.fromEntries(
|
|
3246
|
-
ids.filter((id) => turn.agents[id]?.sessionId).map((id) => [id, turn.agents[id].
|
|
3366
|
+
ids.filter((id) => turn.agents[id]?.session?.sessionId).map((id) => [id, turn.agents[id].session])
|
|
3247
3367
|
);
|
|
3248
3368
|
if (ids.some((id) => turn.leaderIds.includes(id))) {
|
|
3249
3369
|
void startTurn(turn.requirement);
|
|
@@ -3251,6 +3371,7 @@ function App({
|
|
|
3251
3371
|
return;
|
|
3252
3372
|
}
|
|
3253
3373
|
void startTurn(turn.effectiveRequirement ?? turn.requirement, {
|
|
3374
|
+
work: turn.work,
|
|
3254
3375
|
resume: {
|
|
3255
3376
|
agents: ids,
|
|
3256
3377
|
...Object.keys(sessions).length > 0 ? { sessions } : {}
|
|
@@ -3831,7 +3952,7 @@ function App({
|
|
|
3831
3952
|
// the chrome below it off the screen.
|
|
3832
3953
|
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, height: layout.railRows, overflow: "hidden", flexShrink: 0, children: /* @__PURE__ */ jsx6(PipelineRail, { turn: state, spinner }) })
|
|
3833
3954
|
) : null,
|
|
3834
|
-
state.reason ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children:
|
|
3955
|
+
state.reason || state.routing ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: (state.routing ? " route: " + state.routing.mode + " \xB7 " + state.routing.workId + (state.routing.reroutes ? " \xB7 rerouted" : "") : "") + (state.reason ? " reason: " + state.reason : "") }) : null,
|
|
3835
3956
|
/* @__PURE__ */ jsx6(
|
|
3836
3957
|
Box6,
|
|
3837
3958
|
{
|
|
@@ -3978,6 +4099,7 @@ export {
|
|
|
3978
4099
|
conversationTurns,
|
|
3979
4100
|
createInitialState,
|
|
3980
4101
|
initialState,
|
|
4102
|
+
latestWorkFromTurns,
|
|
3981
4103
|
leaderAgentIds,
|
|
3982
4104
|
measureRailRows,
|
|
3983
4105
|
plannedAgents,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiclawcli/dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tui": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@multiclawcli/core": "0.
|
|
28
|
+
"@multiclawcli/core": "0.14.0",
|
|
29
29
|
"ink": "^8.0.0",
|
|
30
30
|
"md4x": "^0.0.30",
|
|
31
31
|
"react": "^19.3.1",
|