@multiclawcli/dashboard 0.8.2 → 0.10.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.js CHANGED
@@ -4,7 +4,7 @@ import { render } from "ink";
4
4
  import { createElement } from "react";
5
5
 
6
6
  // src/tui/App.tsx
7
- import * as path4 from "path";
7
+ import * as path5 from "path";
8
8
  import {
9
9
  AgentSessionManager,
10
10
  describeResumePlan,
@@ -38,6 +38,22 @@ function pendingWrite(target) {
38
38
  return (writes.get(target) ?? Promise.resolve()).catch(() => {
39
39
  });
40
40
  }
41
+ function appendLines(target, lines) {
42
+ if (lines.length === 0) return pendingWrite(target);
43
+ const payload = `${lines.join("\n")}
44
+ `;
45
+ const write = (writes.get(target) ?? Promise.resolve()).catch(() => {
46
+ }).then(async () => {
47
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
48
+ await fs.promises.appendFile(target, payload, "utf-8");
49
+ });
50
+ writes.set(target, write);
51
+ void write.finally(() => {
52
+ if (writes.get(target) === write) writes.delete(target);
53
+ }).catch(() => {
54
+ });
55
+ return write;
56
+ }
41
57
 
42
58
  // src/tui/commands.ts
43
59
  var COMMANDS = [
@@ -55,6 +71,11 @@ var COMMANDS = [
55
71
  description: "Browse finished turns, or continue one",
56
72
  availability: "always"
57
73
  },
74
+ {
75
+ name: "sessions",
76
+ description: "Switch between, or delete, saved sessions",
77
+ availability: "always"
78
+ },
58
79
  {
59
80
  name: "verbose",
60
81
  description: "Toggle tool detail in the stream",
@@ -101,9 +122,9 @@ var COMMANDS = [
101
122
  availability: "running"
102
123
  },
103
124
  {
104
- name: "task",
125
+ name: "new",
105
126
  args: "<requirement>",
106
- description: "Start a new task with fresh agent sessions",
127
+ description: "Start a new session with fresh agent sessions",
107
128
  availability: "idle"
108
129
  },
109
130
  {
@@ -123,7 +144,17 @@ var COMMANDS = [
123
144
  description: "Pause queued turns, not the active agent",
124
145
  availability: "always"
125
146
  },
126
- { name: "continue", description: "Resume queued turns after a checkpoint", availability: "idle" },
147
+ {
148
+ name: "continue",
149
+ description: "Resume queued turns or pass a checkpoint",
150
+ availability: "always"
151
+ },
152
+ {
153
+ name: "modify",
154
+ args: "<instruction>",
155
+ description: "Provide feedback at a checkpoint and continue",
156
+ availability: "running"
157
+ },
127
158
  {
128
159
  name: "respond",
129
160
  args: "<question id> <JSON object>",
@@ -316,6 +347,15 @@ function formatDuration(ms) {
316
347
  const hours = Math.floor(minutes / 60);
317
348
  return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
318
349
  }
350
+ function formatAgo(timestamp, now) {
351
+ const seconds = Math.max(0, Math.floor((now - timestamp) / 1e3));
352
+ if (seconds < 60) return "just now";
353
+ const minutes = Math.floor(seconds / 60);
354
+ if (minutes < 60) return `${minutes}m ago`;
355
+ const hours = Math.floor(minutes / 60);
356
+ if (hours < 24) return `${hours}h ago`;
357
+ return `${Math.floor(hours / 24)}d ago`;
358
+ }
319
359
  var CIRCLED = "\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u246A\u246B\u246C\u246D\u246E\u246F\u2470\u2471\u2472\u2473";
320
360
  function circled(n) {
321
361
  return CIRCLED[n - 1] ?? `(${n})`;
@@ -1035,8 +1075,42 @@ ${action.text}
1035
1075
  return state.view ? { ...state, view: void 0 } : state;
1036
1076
  case "verbose:toggle":
1037
1077
  return { ...state, verbose: !state.verbose };
1078
+ case "session:reset":
1079
+ return seededState(state, action.agents, action.leaderIds, []);
1080
+ case "session:load":
1081
+ return seededState(state, action.agents, action.leaderIds, action.history);
1038
1082
  }
1039
1083
  }
1084
+ function seededState(state, agents, leaderIds, history) {
1085
+ const ids = Object.keys(agents);
1086
+ return {
1087
+ phase: "idle",
1088
+ name: state.name,
1089
+ turn: history[history.length - 1]?.turn ?? 0,
1090
+ requirement: void 0,
1091
+ effectiveRequirement: void 0,
1092
+ startedAt: void 0,
1093
+ endedAt: void 0,
1094
+ cancelled: void 0,
1095
+ interrupted: void 0,
1096
+ success: void 0,
1097
+ acceptance: void 0,
1098
+ totalDuration: void 0,
1099
+ totalCostUsd: void 0,
1100
+ reason: void 0,
1101
+ warning: void 0,
1102
+ reply: void 0,
1103
+ stages: [],
1104
+ leaderIds,
1105
+ agents,
1106
+ focusId: ids[0],
1107
+ autoFollow: true,
1108
+ scroll: 0,
1109
+ history,
1110
+ view: void 0,
1111
+ verbose: state.verbose
1112
+ };
1113
+ }
1040
1114
  function stageStatus(stage, agents) {
1041
1115
  if (stage.outcome !== void 0) return stage.outcome;
1042
1116
  const statuses = stage.agents.map((a) => agents[a.id]?.status ?? "pending");
@@ -1487,6 +1561,54 @@ function HistoryOverlay({
1487
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}` }) })
1488
1562
  ] });
1489
1563
  }
1564
+ function SessionsOverlay({
1565
+ sessions,
1566
+ selected,
1567
+ currentId,
1568
+ width,
1569
+ maxRows,
1570
+ now
1571
+ }) {
1572
+ const capacity = historyCapacity(maxRows);
1573
+ const start = listWindowStart(selected, sessions.length, capacity);
1574
+ const visible = sessions.slice(start, start + capacity);
1575
+ const range = sessions.length > capacity ? ` \xB7 showing ${start + 1}\u2013${start + visible.length} of ${sessions.length}` : "";
1576
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexShrink: 0, paddingLeft: 2, paddingTop: 1, width, children: [
1577
+ /* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
1578
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "SESSIONS" }),
1579
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1580
+ sessions.length,
1581
+ " total",
1582
+ range
1583
+ ] })
1584
+ ] }),
1585
+ sessions.length === 0 ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No sessions yet." }) }) : /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: visible.map((session, offset) => {
1586
+ const index = start + offset;
1587
+ const focused = index === selected;
1588
+ const current = session.id === currentId;
1589
+ const count = session.turnCount;
1590
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [
1591
+ /* @__PURE__ */ jsxs2(Box2, { children: [
1592
+ /* @__PURE__ */ jsx2(Text2, { color: focused ? COLOR.accent : void 0, children: focused ? `${GLYPH.caret} ` : " " }),
1593
+ /* @__PURE__ */ jsxs2(Text2, { bold: focused, color: focused ? COLOR.accent : void 0, children: [
1594
+ current ? "\u25CF " : "",
1595
+ truncate(session.title || "(untitled session)", Math.max(8, width - 30))
1596
+ ] })
1597
+ ] }),
1598
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1599
+ " ",
1600
+ count,
1601
+ " turn",
1602
+ count === 1 ? "" : "s",
1603
+ " \xB7 ",
1604
+ formatAgo(session.lastActiveAt, now),
1605
+ current ? " \xB7 current" : ""
1606
+ ] })
1607
+ ] }, session.id);
1608
+ }) }),
1609
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 select \xB7 enter open \xB7 d delete \xB7 esc close" }) })
1610
+ ] });
1611
+ }
1490
1612
 
1491
1613
  // src/tui/components/interaction.tsx
1492
1614
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -1906,13 +2028,93 @@ function CommandMenu({ rows, selected }) {
1906
2028
  }
1907
2029
 
1908
2030
  // src/tui/history.ts
2031
+ import * as fs3 from "fs";
2032
+ import * as path3 from "path";
2033
+
2034
+ // src/tui/sessions.ts
2035
+ import { randomUUID } from "crypto";
1909
2036
  import * as fs2 from "fs";
1910
2037
  import * as path2 from "path";
1911
- var CONSOLE_DIR = "console";
1912
- var HISTORY_FILE = "history.json";
2038
+ var SESSIONS_DIR = "sessions";
2039
+ var INDEX_FILE = "index.json";
2040
+ var MAX_TITLE_CHARS = 80;
2041
+ function sessionsRoot(workDir) {
2042
+ return path2.join(workDir, ".multiclaw", SESSIONS_DIR);
2043
+ }
2044
+ function sessionDir(workDir, id) {
2045
+ return path2.join(sessionsRoot(workDir), id);
2046
+ }
2047
+ function indexPath(workDir) {
2048
+ return path2.join(sessionsRoot(workDir), INDEX_FILE);
2049
+ }
2050
+ function isSessionMeta(value) {
2051
+ if (typeof value !== "object" || value === null) return false;
2052
+ const meta = value;
2053
+ return typeof meta.id === "string" && typeof meta.title === "string" && typeof meta.createdAt === "number" && typeof meta.lastActiveAt === "number" && typeof meta.turnCount === "number";
2054
+ }
2055
+ function listSessions(workDir) {
2056
+ try {
2057
+ const parsed = JSON.parse(fs2.readFileSync(indexPath(workDir), "utf-8"));
2058
+ if (!Array.isArray(parsed)) return [];
2059
+ return parsed.filter(isSessionMeta).sort((a, b) => b.lastActiveAt - a.lastActiveAt);
2060
+ } catch {
2061
+ return [];
2062
+ }
2063
+ }
2064
+ function latestSession(workDir) {
2065
+ return listSessions(workDir)[0];
2066
+ }
2067
+ function mutateIndex(workDir, fn) {
2068
+ const next = fn(readIndex(workDir));
2069
+ const target = indexPath(workDir);
2070
+ fs2.mkdirSync(path2.dirname(target), { recursive: true });
2071
+ const tmp = `${target}.tmp`;
2072
+ fs2.writeFileSync(tmp, JSON.stringify(next), "utf-8");
2073
+ fs2.renameSync(tmp, target);
2074
+ }
2075
+ function readIndex(workDir) {
2076
+ try {
2077
+ const parsed = JSON.parse(fs2.readFileSync(indexPath(workDir), "utf-8"));
2078
+ return Array.isArray(parsed) ? parsed.filter(isSessionMeta) : [];
2079
+ } catch {
2080
+ return [];
2081
+ }
2082
+ }
2083
+ function createSession(workDir) {
2084
+ const now = Date.now();
2085
+ const meta = {
2086
+ id: randomUUID(),
2087
+ title: "",
2088
+ createdAt: now,
2089
+ lastActiveAt: now,
2090
+ turnCount: 0
2091
+ };
2092
+ mutateIndex(workDir, (metas) => [meta, ...metas]);
2093
+ return meta;
2094
+ }
2095
+ function deleteSession(workDir, id) {
2096
+ fs2.rmSync(sessionDir(workDir, id), { recursive: true, force: true });
2097
+ mutateIndex(workDir, (metas) => metas.filter((meta) => meta.id !== id));
2098
+ }
2099
+ function touchSession(workDir, id, patch) {
2100
+ mutateIndex(
2101
+ workDir,
2102
+ (metas) => metas.map(
2103
+ (meta) => meta.id === id ? {
2104
+ ...meta,
2105
+ ...patch.title !== void 0 ? { title: patch.title.slice(0, MAX_TITLE_CHARS) } : {},
2106
+ ...patch.turnCount !== void 0 ? { turnCount: patch.turnCount } : {},
2107
+ lastActiveAt: patch.lastActiveAt ?? Date.now()
2108
+ } : meta
2109
+ )
2110
+ );
2111
+ }
2112
+
2113
+ // src/tui/history.ts
2114
+ var TURNS_FILE = "turns.jsonl";
1913
2115
  var EXPORT_DIR = "exports";
1914
- function historyPath(workDir) {
1915
- return path2.join(workDir, ".multiclaw", CONSOLE_DIR, HISTORY_FILE);
2116
+ function turnsPath(workDir, sessionId) {
2117
+ return path3.join(sessionDir(workDir, sessionId), TURNS_FILE);
1916
2118
  }
1917
2119
  function isObject(value) {
1918
2120
  return typeof value === "object" && value !== null;
@@ -1942,25 +2144,36 @@ function isTurnView(value) {
1942
2144
  ))
1943
2145
  ) && Array.isArray(turn.leaderIds);
1944
2146
  }
1945
- function loadHistory(workDir) {
2147
+ function loadTurns(workDir, sessionId) {
1946
2148
  try {
1947
- const raw = fs2.readFileSync(historyPath(workDir), "utf-8");
1948
- const parsed = JSON.parse(raw);
1949
- if (!Array.isArray(parsed)) return [];
1950
- return parsed.filter(isTurnView).slice(-MAX_HISTORY);
2149
+ const raw = fs3.readFileSync(turnsPath(workDir, sessionId), "utf-8");
2150
+ const turns = [];
2151
+ for (const line of raw.split("\n")) {
2152
+ if (line.trim() === "") continue;
2153
+ try {
2154
+ const parsed = JSON.parse(line);
2155
+ if (isTurnView(parsed)) turns.push(parsed);
2156
+ } catch {
2157
+ }
2158
+ }
2159
+ return turns.slice(-MAX_HISTORY);
1951
2160
  } catch {
1952
2161
  return [];
1953
2162
  }
1954
2163
  }
1955
- function saveHistory(workDir, turns) {
1956
- return writeJsonFile(historyPath(workDir), JSON.stringify(turns));
2164
+ function appendTurns(workDir, sessionId, turns) {
2165
+ if (turns.length === 0) return Promise.resolve();
2166
+ return appendLines(
2167
+ turnsPath(workDir, sessionId),
2168
+ turns.map((turn) => JSON.stringify(turn))
2169
+ );
1957
2170
  }
1958
- function exportPath(workDir, turn) {
1959
- return path2.join(workDir, ".multiclaw", CONSOLE_DIR, EXPORT_DIR, `turn-${turn}.md`);
2171
+ function exportPath(workDir, sessionId, turn) {
2172
+ return path3.join(sessionDir(workDir, sessionId), EXPORT_DIR, `turn-${turn}.md`);
1960
2173
  }
1961
2174
  async function exportTurn(turn, filePath) {
1962
- await fs2.promises.mkdir(path2.dirname(filePath), { recursive: true });
1963
- await fs2.promises.writeFile(filePath, formatTurnMarkdown(turn), "utf-8");
2175
+ await fs3.promises.mkdir(path3.dirname(filePath), { recursive: true });
2176
+ await fs3.promises.writeFile(filePath, formatTurnMarkdown(turn), "utf-8");
1964
2177
  }
1965
2178
  function formatTurnMarkdown(turn) {
1966
2179
  const lines = [`# Turn ${turn.turn}`, ""];
@@ -2002,13 +2215,13 @@ function formatTurnMarkdown(turn) {
2002
2215
  }
2003
2216
 
2004
2217
  // src/tui/session.ts
2005
- import * as fs3 from "fs";
2006
- import * as path3 from "path";
2218
+ import * as fs4 from "fs";
2219
+ import * as path4 from "path";
2007
2220
  var SESSION_FILE = "session.json";
2008
2221
  var SESSION_VERSION = 1;
2009
2222
  var MAX_JOURNAL_LOG_CHARS = 1200;
2010
- function sessionPath(workDir) {
2011
- return path3.join(workDir, ".multiclaw", CONSOLE_DIR, SESSION_FILE);
2223
+ function sessionPath(workDir, sessionId) {
2224
+ return path4.join(sessionDir(workDir, sessionId), SESSION_FILE);
2012
2225
  }
2013
2226
  function isObject2(value) {
2014
2227
  return typeof value === "object" && value !== null;
@@ -2030,14 +2243,13 @@ function isJournalTurn(value) {
2030
2243
  if (!isObject2(value)) return false;
2031
2244
  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));
2032
2245
  }
2033
- function loadJournal(workDir) {
2246
+ function loadJournal(workDir, sessionId) {
2034
2247
  try {
2035
- const parsed = JSON.parse(fs3.readFileSync(sessionPath(workDir), "utf-8"));
2248
+ const parsed = JSON.parse(fs4.readFileSync(sessionPath(workDir, sessionId), "utf-8"));
2036
2249
  if (!isObject2(parsed) || parsed.version !== SESSION_VERSION) return void 0;
2037
2250
  const queue = Array.isArray(parsed.queue) ? parsed.queue.filter((item) => typeof item === "string" && item !== "") : [];
2038
2251
  return {
2039
2252
  version: SESSION_VERSION,
2040
- ...Number.isInteger(parsed.taskStart) && parsed.taskStart >= 0 ? { taskStart: parsed.taskStart } : {},
2041
2253
  ...queue.length > 0 ? { queue } : {},
2042
2254
  ...isJournalTurn(parsed.inFlight) ? { inFlight: parsed.inFlight } : {}
2043
2255
  };
@@ -2046,11 +2258,11 @@ function loadJournal(workDir) {
2046
2258
  }
2047
2259
  }
2048
2260
  function isEmpty(journal) {
2049
- return journal.inFlight === void 0 && journal.queue === void 0 && !journal.taskStart;
2261
+ return journal.inFlight === void 0 && journal.queue === void 0;
2050
2262
  }
2051
- function saveJournal(workDir, journal) {
2052
- const target = sessionPath(workDir);
2053
- if (isEmpty(journal) && !fs3.existsSync(target)) return Promise.resolve();
2263
+ function saveJournal(workDir, sessionId, journal) {
2264
+ const target = sessionPath(workDir, sessionId);
2265
+ if (isEmpty(journal) && !fs4.existsSync(target)) return Promise.resolve();
2054
2266
  return writeJsonFile(target, JSON.stringify(journal));
2055
2267
  }
2056
2268
  function tail(text, max) {
@@ -2104,11 +2316,12 @@ function interruptedTurn(entry) {
2104
2316
  function plural(count, word) {
2105
2317
  return count === 1 ? word : `${word}s`;
2106
2318
  }
2107
- function restoreConsole(workDir) {
2108
- const history = loadHistory(workDir);
2109
- const journal = loadJournal(workDir);
2319
+ function restoreConsole(workDir, sessionId) {
2320
+ const history = loadTurns(workDir, sessionId);
2321
+ const journal = loadJournal(workDir, sessionId);
2110
2322
  const inFlight = journal?.inFlight;
2111
2323
  const last = history[history.length - 1];
2324
+ const persistedTurn = last?.turn ?? 0;
2112
2325
  const parts = [];
2113
2326
  if (inFlight && (!last || inFlight.turn > last.turn)) {
2114
2327
  history.push(interruptedTurn(inFlight));
@@ -2128,7 +2341,7 @@ function restoreConsole(workDir) {
2128
2341
  return {
2129
2342
  history: restored,
2130
2343
  queue,
2131
- taskStart: journal?.taskStart ?? 0,
2344
+ persistedTurn,
2132
2345
  // Read from the *final* last turn: a recovered interrupted turn has no
2133
2346
  // reply, so it correctly clears the "waiting for an answer" state.
2134
2347
  awaitingAnswer: restored[restored.length - 1]?.reply?.mode === "ask",
@@ -2225,12 +2438,41 @@ function StatusLine({
2225
2438
  stages > 0 ? ` \xB7 ${stages} stages` : ""
2226
2439
  ] });
2227
2440
  }
2228
- function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }) {
2441
+ function App({
2442
+ config,
2443
+ onTurnStart,
2444
+ sessions: providedSessions,
2445
+ onCleanupReady,
2446
+ initialSession
2447
+ }) {
2229
2448
  const { exit } = useApp();
2230
2449
  const { columns, rows } = useWindowSize();
2450
+ const sessionIdRef = useRef(null);
2451
+ const launchNoticeRef = useRef(void 0);
2452
+ const openPickerOnMountRef = useRef(false);
2453
+ if (!sessionIdRef.current) {
2454
+ const latest = () => latestSession(config.workDir)?.id ?? createSession(config.workDir).id;
2455
+ if (initialSession?.kind === "new") {
2456
+ sessionIdRef.current = createSession(config.workDir).id;
2457
+ } else if (initialSession?.kind === "resume" && initialSession.id) {
2458
+ const target = initialSession.id;
2459
+ if (listSessions(config.workDir).some((s) => s.id === target)) {
2460
+ sessionIdRef.current = target;
2461
+ } else {
2462
+ sessionIdRef.current = latest();
2463
+ launchNoticeRef.current = `session "${target}" not found \u2014 opened the most recent instead`;
2464
+ }
2465
+ } else {
2466
+ sessionIdRef.current = latest();
2467
+ if (initialSession?.kind === "resume") openPickerOnMountRef.current = true;
2468
+ }
2469
+ }
2231
2470
  const restoredRef = useRef(null);
2232
- if (!restoredRef.current) restoredRef.current = restoreConsole(config.workDir);
2471
+ if (!restoredRef.current) {
2472
+ restoredRef.current = restoreConsole(config.workDir, sessionIdRef.current);
2473
+ }
2233
2474
  const restored = restoredRef.current;
2475
+ const persistedTurnRef = useRef(restored.persistedTurn);
2234
2476
  const [state, dispatch] = useReducer(
2235
2477
  tuiReducer,
2236
2478
  config,
@@ -2247,10 +2489,15 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2247
2489
  queueRef.current = queue;
2248
2490
  const [cancelled, setCancelled] = useState(false);
2249
2491
  const [error, setError] = useState();
2250
- const [notice, setNotice] = useState(restored.notice);
2492
+ const [notice, setNotice] = useState(
2493
+ launchNoticeRef.current ?? restored.notice
2494
+ );
2251
2495
  const [helpOpen, setHelpOpen] = useState(false);
2252
2496
  const [historyOpen, setHistoryOpen] = useState(false);
2253
2497
  const [historyCursor, setHistoryCursor] = useState(0);
2498
+ const [sessionsOpen, setSessionsOpen] = useState(false);
2499
+ const [sessionCursor, setSessionCursor] = useState(0);
2500
+ const [sessionList, setSessionList] = useState([]);
2254
2501
  const [menuCursor, setMenuCursor] = useState({
2255
2502
  query: void 0,
2256
2503
  index: 0
@@ -2264,7 +2511,6 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2264
2511
  const closingRef = useRef(false);
2265
2512
  const shutdownRef = useRef(null);
2266
2513
  const busyRef = useRef(false);
2267
- const taskStartRef = useRef(restored.taskStart);
2268
2514
  const turnSerialRef = useRef(0);
2269
2515
  const [awaitingAnswer, setAwaitingAnswer] = useState(restored.awaitingAnswer);
2270
2516
  const [queuePaused, setQueuePaused] = useState(restored.queue.length > 0);
@@ -2273,6 +2519,8 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2273
2519
  const pending = useRef([]);
2274
2520
  const permissionSerialRef = useRef(0);
2275
2521
  const questionSerialRef = useRef(0);
2522
+ const checkpointResolveRef = useRef(null);
2523
+ const checkpointLabelRef = useRef("");
2276
2524
  const mountedRef = useRef(true);
2277
2525
  const interactionsRef = useRef([]);
2278
2526
  const [interactions, setInteractions] = useState([]);
@@ -2325,6 +2573,22 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2325
2573
  if (mountedRef.current) setInteractions([...interactionsRef.current]);
2326
2574
  });
2327
2575
  }, []);
2576
+ const handleCheckpoint = useCallback((info, signal) => {
2577
+ if (closingRef.current || signal.aborted) return Promise.resolve({ action: "cancel" });
2578
+ return new Promise((resolve2) => {
2579
+ checkpointResolveRef.current = resolve2;
2580
+ checkpointLabelRef.current = info.label;
2581
+ const names = info.completedAgents.map((a) => a.name).join(", ");
2582
+ setNotice(
2583
+ `checkpoint: ${info.label} \u2014 ${names} done \xB7 /continue /modify <instruction> /cancel`
2584
+ );
2585
+ const onAbort = () => {
2586
+ checkpointResolveRef.current = null;
2587
+ resolve2({ action: "cancel" });
2588
+ };
2589
+ signal.addEventListener("abort", onAbort, { once: true });
2590
+ });
2591
+ }, []);
2328
2592
  const clearInteractions = useCallback(() => {
2329
2593
  for (const entry of [...interactionsRef.current]) entry.settle();
2330
2594
  }, []);
@@ -2347,16 +2611,21 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2347
2611
  const timer = setInterval(() => setCaretOn((on) => !on), BLINK_INTERVAL_MS);
2348
2612
  return () => clearInterval(timer);
2349
2613
  }, [running, line.text]);
2350
- const lastSaved = useRef(void 0);
2351
2614
  useEffect(() => {
2352
- const turns = recordedTurns(stateRef.current).slice(-MAX_HISTORY);
2353
- if (turns.length === 0) return;
2354
- const json = JSON.stringify(turns);
2355
- if (json === lastSaved.current) return;
2356
- lastSaved.current = json;
2357
- void saveHistory(config.workDir, turns).catch(
2615
+ const sessionId = sessionIdRef.current;
2616
+ if (!sessionId) return;
2617
+ const turns = recordedTurns(stateRef.current);
2618
+ const fresh = turns.filter((turn) => turn.turn > persistedTurnRef.current);
2619
+ if (fresh.length === 0) return;
2620
+ persistedTurnRef.current = fresh[fresh.length - 1].turn;
2621
+ void appendTurns(config.workDir, sessionId, fresh).catch(
2358
2622
  (err) => setNotice(`history not saved: ${err.message}`)
2359
2623
  );
2624
+ const title = turns.find((turn) => turn.requirement)?.requirement;
2625
+ touchSession(config.workDir, sessionId, {
2626
+ turnCount: fresh[fresh.length - 1].turn,
2627
+ ...title ? { title } : {}
2628
+ });
2360
2629
  }, [state.history, state.phase]);
2361
2630
  const persistSession = useCallback(() => {
2362
2631
  if (closingRef.current) return;
@@ -2366,15 +2635,16 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2366
2635
  if (entry.kind === "permission") waiting.permissions += 1;
2367
2636
  else waiting.questions += 1;
2368
2637
  }
2638
+ const sessionId = sessionIdRef.current;
2639
+ if (!sessionId) return;
2369
2640
  const journal = {
2370
2641
  version: SESSION_VERSION,
2371
- taskStart: taskStartRef.current,
2372
2642
  ...queueRef.current.length > 0 ? { queue: [...queueRef.current] } : {},
2373
2643
  // Only a turn that is actually running is in flight. Writing one for an
2374
2644
  // idle console would invent work the next session would then "recover".
2375
2645
  ...current.phase === "running" && current.requirement !== void 0 ? { inFlight: journalTurn(current, waiting) } : {}
2376
2646
  };
2377
- void saveJournal(config.workDir, journal).catch((err) => {
2647
+ void saveJournal(config.workDir, sessionId, journal).catch((err) => {
2378
2648
  if (mountedRef.current) setNotice(`session not saved: ${err.message}`);
2379
2649
  });
2380
2650
  }, [config.workDir]);
@@ -2396,10 +2666,10 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2396
2666
  if (closingRef.current || busyRef.current) return;
2397
2667
  busyRef.current = true;
2398
2668
  setRunning(true);
2399
- const continuation = opts?.newTask ? null : continuationRef.current;
2669
+ const continuation = opts?.newSession ? null : continuationRef.current;
2400
2670
  continuationRef.current = null;
2401
2671
  const resume = opts?.resume ?? continuationResume(continuation, sessionsRef.current);
2402
- if (opts?.newTask) {
2672
+ if (opts?.newSession) {
2403
2673
  try {
2404
2674
  await sessionsRef.current?.reset();
2405
2675
  if (closingRef.current) {
@@ -2407,7 +2677,14 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2407
2677
  setRunning(false);
2408
2678
  return;
2409
2679
  }
2410
- taskStartRef.current = stateRef.current.turn;
2680
+ const created = createSession(config.workDir);
2681
+ sessionIdRef.current = created.id;
2682
+ persistedTurnRef.current = 0;
2683
+ dispatch({
2684
+ type: "session:reset",
2685
+ agents: seedAgents(config),
2686
+ leaderIds: leaderAgentIds(config)
2687
+ });
2411
2688
  } catch (err) {
2412
2689
  busyRef.current = false;
2413
2690
  setRunning(false);
@@ -2425,11 +2702,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2425
2702
  setError(void 0);
2426
2703
  setCancelled(false);
2427
2704
  setNotice(void 0);
2428
- const earlierTurns = conversationTurns({
2429
- ...stateRef.current,
2430
- history: stateRef.current.history.filter((turn) => turn.turn > taskStartRef.current),
2431
- ...stateRef.current.turn <= taskStartRef.current ? { phase: "idle" } : {}
2432
- });
2705
+ const earlierTurns = opts?.newSession ? [] : conversationTurns(stateRef.current);
2433
2706
  dispatch({
2434
2707
  type: "turn:start",
2435
2708
  requirement,
@@ -2438,7 +2711,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2438
2711
  startedAt: Date.now()
2439
2712
  });
2440
2713
  const stamp = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${++turnSerialRef.current}`;
2441
- const logDir = path4.join(config.workDir, ".multiclaw", "runs", `run-${stamp}`, "logs");
2714
+ const logDir = path5.join(config.workDir, ".multiclaw", "runs", `run-${stamp}`, "logs");
2442
2715
  const runConfig = {
2443
2716
  ...config,
2444
2717
  context: { ...config.context, requirement },
@@ -2448,6 +2721,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2448
2721
  logDir,
2449
2722
  permissions: !config.permissions || config.permissions.mode === "ask" ? { mode: "ask", onRequest: requestPermission } : config.permissions,
2450
2723
  questions: config.questions?.onRequest ? config.questions : { onRequest: requestQuestion },
2724
+ onCheckpoint: config.onCheckpoint ?? handleCheckpoint,
2451
2725
  // A retry names the agents to redo; a continued conversation carries only
2452
2726
  // sessions and still lets the leader route. Both arrive here.
2453
2727
  ...resume ? { resume } : {}
@@ -2503,7 +2777,15 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2503
2777
  activeTurnRef.current = null;
2504
2778
  }
2505
2779
  },
2506
- [config, flush, onTurnStart, requestPermission, requestQuestion, clearInteractions]
2780
+ [
2781
+ config,
2782
+ flush,
2783
+ onTurnStart,
2784
+ requestPermission,
2785
+ requestQuestion,
2786
+ handleCheckpoint,
2787
+ clearInteractions
2788
+ ]
2507
2789
  );
2508
2790
  useEffect(() => {
2509
2791
  if (running || busyRef.current || closingRef.current || queuePaused || awaitingAnswer || queue.length === 0)
@@ -2512,16 +2794,53 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2512
2794
  setQueue(rest);
2513
2795
  void startTurn(next);
2514
2796
  }, [running, queue, queuePaused, awaitingAnswer, startTurn]);
2797
+ const switchSession = useCallback(
2798
+ async (id) => {
2799
+ if (id === sessionIdRef.current || busyRef.current || closingRef.current) return;
2800
+ try {
2801
+ await sessionsRef.current?.reset();
2802
+ } catch (err) {
2803
+ setError(`session switch failed: ${err.message}`);
2804
+ return;
2805
+ }
2806
+ if (closingRef.current) return;
2807
+ const restored2 = restoreConsole(config.workDir, id);
2808
+ sessionIdRef.current = id;
2809
+ persistedTurnRef.current = restored2.persistedTurn;
2810
+ const resumable = [...restored2.history].reverse().find((turn) => Object.keys(turnSessions(turn)).length > 0);
2811
+ continuationRef.current = resumable ? { turn: resumable.turn, sessions: turnSessions(resumable) } : null;
2812
+ dispatch({
2813
+ type: "session:load",
2814
+ agents: seedAgents(config),
2815
+ leaderIds: leaderAgentIds(config),
2816
+ history: restored2.history
2817
+ });
2818
+ setQueue(restored2.queue);
2819
+ queuePausedRef.current = restored2.queue.length > 0;
2820
+ setQueuePaused(restored2.queue.length > 0);
2821
+ awaitingAnswerRef.current = restored2.awaitingAnswer;
2822
+ setAwaitingAnswer(restored2.awaitingAnswer);
2823
+ setError(void 0);
2824
+ setCancelled(false);
2825
+ setNotice(
2826
+ restored2.notice ?? (resumable ? `switched session \xB7 continue with your next instruction` : `switched session \xB7 ${restored2.history.length} turn${restored2.history.length === 1 ? "" : "s"}`)
2827
+ );
2828
+ },
2829
+ [config]
2830
+ );
2515
2831
  const cleanup = useCallback(async () => {
2516
2832
  closingRef.current = true;
2517
2833
  abortRef.current?.abort();
2518
2834
  clearInteractions();
2519
2835
  await activeTurnRef.current;
2520
2836
  await sessionsRef.current?.close();
2521
- await Promise.all([
2522
- pendingWrite(historyPath(config.workDir)),
2523
- pendingWrite(sessionPath(config.workDir))
2524
- ]);
2837
+ const sessionId = sessionIdRef.current;
2838
+ if (sessionId) {
2839
+ await Promise.all([
2840
+ pendingWrite(turnsPath(config.workDir, sessionId)),
2841
+ pendingWrite(sessionPath(config.workDir, sessionId))
2842
+ ]);
2843
+ }
2525
2844
  if (reporterErrorRef.current) throw reporterErrorRef.current;
2526
2845
  }, [clearInteractions, config.workDir]);
2527
2846
  useEffect(() => {
@@ -2533,6 +2852,15 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2533
2852
  });
2534
2853
  };
2535
2854
  }, [cleanup, onCleanupReady]);
2855
+ useEffect(() => {
2856
+ if (!openPickerOnMountRef.current) return;
2857
+ openPickerOnMountRef.current = false;
2858
+ const list = listSessions(config.workDir);
2859
+ setSessionList(list);
2860
+ const current = list.findIndex((session) => session.id === sessionIdRef.current);
2861
+ setSessionCursor(current === -1 ? 0 : current);
2862
+ setSessionsOpen(true);
2863
+ }, []);
2536
2864
  const shutdown = useCallback(() => {
2537
2865
  if (shutdownRef.current) return shutdownRef.current;
2538
2866
  closingRef.current = true;
@@ -2545,13 +2873,20 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2545
2873
  shutdownRef.current = (async () => {
2546
2874
  try {
2547
2875
  await cleanup();
2548
- const turns = recordedTurns(stateRef.current).slice(-MAX_HISTORY);
2549
- if (turns.length) await saveHistory(config.workDir, turns);
2550
- await saveJournal(config.workDir, {
2551
- version: SESSION_VERSION,
2552
- taskStart: taskStartRef.current,
2553
- ...leftover.length > 0 ? { queue: leftover } : {}
2554
- });
2876
+ const sessionId = sessionIdRef.current;
2877
+ if (sessionId) {
2878
+ const fresh = recordedTurns(stateRef.current).filter(
2879
+ (turn) => turn.turn > persistedTurnRef.current
2880
+ );
2881
+ if (fresh.length) {
2882
+ persistedTurnRef.current = fresh[fresh.length - 1].turn;
2883
+ await appendTurns(config.workDir, sessionId, fresh);
2884
+ }
2885
+ await saveJournal(config.workDir, sessionId, {
2886
+ version: SESSION_VERSION,
2887
+ ...leftover.length > 0 ? { queue: leftover } : {}
2888
+ });
2889
+ }
2555
2890
  exit();
2556
2891
  } catch (err) {
2557
2892
  setError(`shutdown failed: ${err.message}`);
@@ -2580,8 +2915,8 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2580
2915
  const paletteTotal = allRows.length;
2581
2916
  const menuIndex = menuCursor.query === paletteKey ? menuCursor.index : 0;
2582
2917
  const selected = paletteTotal === 0 ? 0 : Math.min(menuIndex, paletteTotal - 1);
2583
- const menuOpen = !helpOpen && !historyOpen && query !== void 0 && matches.length > 0;
2584
- const argOpen = !helpOpen && !historyOpen && arg !== void 0 && argRows.length > 0;
2918
+ const menuOpen = !helpOpen && !historyOpen && !sessionsOpen && query !== void 0 && matches.length > 0;
2919
+ const argOpen = !helpOpen && !historyOpen && !sessionsOpen && arg !== void 0 && argRows.length > 0;
2585
2920
  const paletteOpen = menuOpen || argOpen;
2586
2921
  const front = interactions[0];
2587
2922
  const menu = front?.kind === "permission" && permissionMenu.id === front.shortId ? permissionMenu : { id: front?.shortId ?? "", cursor: void 0, released: false };
@@ -2602,7 +2937,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2602
2937
  columns,
2603
2938
  rows,
2604
2939
  turn: state.turn,
2605
- overlay: helpOpen || historyOpen,
2940
+ overlay: helpOpen || historyOpen || sessionsOpen,
2606
2941
  menuRows: paletteOpen ? paletteTotal : 0,
2607
2942
  queueRows: queue.length,
2608
2943
  interactionRows: interactionRows(interactions, Math.max(MIN_COLUMNS, columns)),
@@ -2637,6 +2972,12 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2637
2972
  const runCommand = useCallback(
2638
2973
  (name, args, rawText) => {
2639
2974
  setLine(EMPTY_LINE);
2975
+ if (checkpointResolveRef.current && name !== "continue" && name !== "modify" && name !== "cancel") {
2976
+ setNotice(
2977
+ `checkpoint: ${checkpointLabelRef.current} \xB7 /continue /modify <instruction> /cancel`
2978
+ );
2979
+ return;
2980
+ }
2640
2981
  if (!canRun(name, running)) {
2641
2982
  setNotice(unavailableReason(name, running));
2642
2983
  return;
@@ -2742,16 +3083,16 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2742
3083
  setNotice(`selected ${clean(option.name)} for ${entry.shortId}`);
2743
3084
  return;
2744
3085
  }
2745
- case "task": {
3086
+ case "new": {
2746
3087
  const text = args.join(" ");
2747
3088
  if (!text) {
2748
- setNotice("usage: /task <new requirement> \u2014 closes prior agent sessions");
3089
+ setNotice("usage: /new <requirement> \u2014 starts a new session with fresh agent sessions");
2749
3090
  return;
2750
3091
  }
2751
3092
  setQueue([]);
2752
3093
  queuePausedRef.current = false;
2753
3094
  setQueuePaused(false);
2754
- void startTurn(text, { newTask: true });
3095
+ void startTurn(text, { newSession: true });
2755
3096
  return;
2756
3097
  }
2757
3098
  case "answer": {
@@ -2777,7 +3118,15 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2777
3118
  setQueuePaused(true);
2778
3119
  setNotice("queue paused \u2014 the active turn continues");
2779
3120
  return;
2780
- case "continue":
3121
+ case "continue": {
3122
+ if (checkpointResolveRef.current) {
3123
+ const resolve2 = checkpointResolveRef.current;
3124
+ checkpointResolveRef.current = null;
3125
+ checkpointLabelRef.current = "";
3126
+ resolve2({ action: "continue" });
3127
+ setNotice("continuing\u2026");
3128
+ return;
3129
+ }
2781
3130
  if (awaitingAnswerRef.current) {
2782
3131
  setNotice("answer the leader first; queued input is not an answer");
2783
3132
  return;
@@ -2786,6 +3135,24 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2786
3135
  setQueuePaused(false);
2787
3136
  setNotice("queue resumed");
2788
3137
  return;
3138
+ }
3139
+ case "modify": {
3140
+ const instruction = args.join(" ");
3141
+ if (!instruction) {
3142
+ setNotice("usage: /modify <instruction> \u2014 only at a checkpoint");
3143
+ return;
3144
+ }
3145
+ if (!checkpointResolveRef.current) {
3146
+ setNotice("no checkpoint is waiting");
3147
+ return;
3148
+ }
3149
+ const resolve2 = checkpointResolveRef.current;
3150
+ checkpointResolveRef.current = null;
3151
+ checkpointLabelRef.current = "";
3152
+ resolve2({ action: "modify", instruction });
3153
+ setNotice("modifying and continuing\u2026");
3154
+ return;
3155
+ }
2789
3156
  case "agents":
2790
3157
  setNotice(
2791
3158
  `team: ${plannedAgents(config).map((agent) => agent.id).join(", ")}`
@@ -2823,6 +3190,15 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2823
3190
  setHistoryCursor(0);
2824
3191
  setHistoryOpen(true);
2825
3192
  return;
3193
+ case "sessions": {
3194
+ const list = listSessions(config.workDir);
3195
+ setSessionList(list);
3196
+ const current = list.findIndex((session) => session.id === sessionIdRef.current);
3197
+ setSessionCursor(current === -1 ? 0 : current);
3198
+ setNotice(void 0);
3199
+ setSessionsOpen(true);
3200
+ return;
3201
+ }
2826
3202
  case "verbose":
2827
3203
  dispatch({ type: "verbose:toggle" });
2828
3204
  setNotice(state.verbose ? "verbose off" : "verbose on \u2014 showing tool detail");
@@ -2900,7 +3276,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2900
3276
  setNotice(target.error);
2901
3277
  return;
2902
3278
  }
2903
- const file = args[1] ? path4.resolve(config.workDir, args[1]) : exportPath(config.workDir, target.record.turn);
3279
+ const file = args[1] ? path5.resolve(config.workDir, args[1]) : exportPath(config.workDir, sessionIdRef.current ?? "", target.record.turn);
2904
3280
  void exportTurn(target.record, file).then(() => setNotice(`exported turn ${target.record.turn} to ${file}`)).catch((err) => setNotice(`export failed: ${err.message}`));
2905
3281
  return;
2906
3282
  }
@@ -2924,6 +3300,14 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2924
3300
  setNotice("showing latest output");
2925
3301
  return;
2926
3302
  case "cancel": {
3303
+ if (checkpointResolveRef.current) {
3304
+ const resolve2 = checkpointResolveRef.current;
3305
+ checkpointResolveRef.current = null;
3306
+ checkpointLabelRef.current = "";
3307
+ resolve2({ action: "cancel" });
3308
+ setNotice("checkpoint cancelled");
3309
+ return;
3310
+ }
2927
3311
  if (!abortRef.current) {
2928
3312
  setNotice("nothing to cancel");
2929
3313
  return;
@@ -2968,6 +3352,10 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
2968
3352
  runCommand(command.name, command.args, text);
2969
3353
  return;
2970
3354
  }
3355
+ if (checkpointResolveRef.current) {
3356
+ runCommand("modify", [text], text);
3357
+ return;
3358
+ }
2971
3359
  setLine(EMPTY_LINE);
2972
3360
  setNotice(void 0);
2973
3361
  if (running || busyRef.current) {
@@ -3009,6 +3397,8 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
3009
3397
  setHelpOpen(false);
3010
3398
  } else if (historyOpen) {
3011
3399
  setHistoryOpen(false);
3400
+ } else if (sessionsOpen) {
3401
+ setSessionsOpen(false);
3012
3402
  } else if (abortRef.current) {
3013
3403
  abortRef.current.abort();
3014
3404
  clearInteractions();
@@ -3054,6 +3444,7 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
3054
3444
  return;
3055
3445
  }
3056
3446
  continuationRef.current = { turn: target.turn, sessions };
3447
+ dispatch({ type: "view:open", turn: target.turn });
3057
3448
  setHistoryOpen(false);
3058
3449
  setNotice(
3059
3450
  `continuing turn ${target.turn} \u2014 ${count} ${count === 1 ? "session" : "sessions"} \xB7 type your next instruction`
@@ -3062,6 +3453,50 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
3062
3453
  }
3063
3454
  return;
3064
3455
  }
3456
+ if (sessionsOpen) {
3457
+ if (key.escape) {
3458
+ setSessionsOpen(false);
3459
+ return;
3460
+ }
3461
+ if (sessionList.length === 0) return;
3462
+ const index = Math.min(sessionCursor, sessionList.length - 1);
3463
+ if (key.upArrow) {
3464
+ setSessionCursor((index - 1 + sessionList.length) % sessionList.length);
3465
+ return;
3466
+ }
3467
+ if (key.downArrow) {
3468
+ setSessionCursor((index + 1) % sessionList.length);
3469
+ return;
3470
+ }
3471
+ if (key.return) {
3472
+ const target = sessionList[index];
3473
+ setSessionsOpen(false);
3474
+ if (target.id === sessionIdRef.current) {
3475
+ setNotice("already on this session");
3476
+ return;
3477
+ }
3478
+ if (running || busyRef.current) {
3479
+ setNotice("finish or cancel the current turn before switching sessions");
3480
+ return;
3481
+ }
3482
+ void switchSession(target.id);
3483
+ return;
3484
+ }
3485
+ if (char === "d") {
3486
+ const target = sessionList[index];
3487
+ if (target.id === sessionIdRef.current) {
3488
+ setNotice("can't delete the current session");
3489
+ return;
3490
+ }
3491
+ deleteSession(config.workDir, target.id);
3492
+ const next = listSessions(config.workDir);
3493
+ setSessionList(next);
3494
+ setSessionCursor(Math.min(index, Math.max(0, next.length - 1)));
3495
+ setNotice(`deleted session \xB7 ${next.length} left`);
3496
+ return;
3497
+ }
3498
+ return;
3499
+ }
3065
3500
  if (key.ctrl && char === "d") {
3066
3501
  if (!line.text) void shutdown();
3067
3502
  return;
@@ -3308,11 +3743,11 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
3308
3743
  const handlerRef = useRef(handleKey);
3309
3744
  handlerRef.current = handleKey;
3310
3745
  useInput((char, key) => handlerRef.current(char, key));
3311
- const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \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 ? interactions.length > 0 ? layout.interactionRows === 0 ? (
3746
+ const keys = helpOpen ? "esc close" : historyOpen ? "\u2191\u2193 select \xB7 enter open \xB7 esc close" : sessionsOpen ? "\u2191\u2193 select \xB7 enter open \xB7 d delete \xB7 esc close" : argOpen ? `\u2191\u2193 select \xB7 tab complete \xB7 enter ${arg?.command.name} \xB7 esc close` : menuOpen ? "\u2191\u2193 select \xB7 tab complete \xB7 enter run \xB7 esc close" : running ? checkpointResolveRef.current ? `/continue \xB7 /modify <instruction> \xB7 /cancel` : interactions.length > 0 ? layout.interactionRows === 0 ? (
3312
3747
  // Too short to dock the bar, so the commands are the only way in.
3313
3748
  "decide with /approve 1 \xB7 /deny \xB7 /respond"
3314
3749
  ) : 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";
3315
- const placeholder = frontQuestion !== void 0 ? formField === void 0 ? "enter to answer" : editingField !== void 0 ? `${editingField.title}${editingField.required ? " *" : ""} \u2014 enter for the next field` : `${formField.title} \u2014 \u2191\u2193 to pick \xB7 enter for the next field` : front?.kind === "permission" && !menu.released ? "choose above \xB7 esc to type here instead" : running ? RUNNING_PLACEHOLDER : awaitingAnswer ? "answer the leader\u2026 \xB7 /task starts a new task" : INPUT_PLACEHOLDER;
3750
+ const placeholder = frontQuestion !== void 0 ? formField === void 0 ? "enter to answer" : editingField !== void 0 ? `${editingField.title}${editingField.required ? " *" : ""} \u2014 enter for the next field` : `${formField.title} \u2014 \u2191\u2193 to pick \xB7 enter for the next field` : front?.kind === "permission" && !menu.released ? "choose above \xB7 esc to type here instead" : checkpointResolveRef.current ? `checkpoint: ${checkpointLabelRef.current} \u2014 type feedback or /continue` : running ? RUNNING_PLACEHOLDER : awaitingAnswer ? "answer the leader\u2026 \xB7 /task starts a new task" : INPUT_PLACEHOLDER;
3316
3751
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: layout.rows, children: [
3317
3752
  /* @__PURE__ */ jsx6(
3318
3753
  Header,
@@ -3346,6 +3781,16 @@ function App({ config, onTurnStart, sessions: providedSessions, onCleanupReady }
3346
3781
  width: layout.columns,
3347
3782
  maxRows: layout.bodyHeight
3348
3783
  }
3784
+ ) : sessionsOpen ? /* @__PURE__ */ jsx6(
3785
+ SessionsOverlay,
3786
+ {
3787
+ sessions: sessionList,
3788
+ selected: Math.min(sessionCursor, Math.max(0, sessionList.length - 1)),
3789
+ currentId: sessionIdRef.current ?? void 0,
3790
+ width: layout.columns,
3791
+ maxRows: layout.bodyHeight,
3792
+ now
3793
+ }
3349
3794
  ) : layout.welcome ? /* @__PURE__ */ jsx6(WelcomePanel, { turn: state, width: layout.columns }) : error ? (
3350
3795
  // The error wins over a reply: a run that stopped with an error is the
3351
3796
  // most urgent thing on screen, and a stale answer underneath it would
@@ -3455,9 +3900,16 @@ async function startTui(options) {
3455
3900
  cleanup = callback;
3456
3901
  };
3457
3902
  try {
3458
- const app = render(createElement(App, { ...options, sessions, onCleanupReady }), {
3459
- exitOnCtrlC: false
3460
- });
3903
+ const app = render(
3904
+ createElement(App, {
3905
+ config: options.config,
3906
+ onTurnStart: options.onTurnStart,
3907
+ initialSession: options.initialSession,
3908
+ sessions,
3909
+ onCleanupReady
3910
+ }),
3911
+ { exitOnCtrlC: false }
3912
+ );
3461
3913
  await app.waitUntilExit();
3462
3914
  } finally {
3463
3915
  try {