@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.
@@ -1,4 +1,4 @@
1
- import type { LeaderMessage } from "@multiclawcli/core";
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, }: {
@@ -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
  /**
@@ -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;
@@ -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
- sessionId?: string;
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
- sessionId: result.sessionId
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 { ...state, agents: { ...state.agents, [result.agentId]: next } };
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(isObject) && Array.isArray(turn.stages) && turn.stages.every(
2143
- (stage) => isObject(stage) && (stage.outcome === void 0 || ["pending", "running", "retrying", "success", "failed", "cancelled", "skipped"].includes(
2144
- stage.outcome
2145
- )) && (stage.memberStatuses === void 0 || isObject(stage.memberStatuses) && Object.values(stage.memberStatuses).every(
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] = UNSETTLED.has(agent.status) ? { ...agent, status: "cancelled" } : { ...agent };
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 continuation = opts?.newSession ? null : continuationRef.current;
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].sessionId])
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: ` reason: ${state.reason}` }) : null,
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.13.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.13.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",