@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/App.d.ts +17 -1
- package/dist/tui/atomic.d.ts +10 -0
- package/dist/tui/components/overlays.d.ts +19 -0
- package/dist/tui/format.d.ts +8 -0
- package/dist/tui/history.d.ts +17 -16
- package/dist/tui/index.d.ts +4 -1
- package/dist/tui/session.d.ts +30 -33
- package/dist/tui/sessions.d.ts +48 -0
- package/dist/tui/state.d.ts +21 -0
- package/dist/tui.js +533 -81
- package/package.json +2 -2
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
|
|
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: "
|
|
125
|
+
name: "new",
|
|
105
126
|
args: "<requirement>",
|
|
106
|
-
description: "Start a new
|
|
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
|
-
{
|
|
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
|
|
1912
|
-
var
|
|
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
|
|
1915
|
-
return
|
|
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
|
|
2147
|
+
function loadTurns(workDir, sessionId) {
|
|
1946
2148
|
try {
|
|
1947
|
-
const raw =
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
|
1956
|
-
|
|
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
|
|
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
|
|
1963
|
-
await
|
|
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
|
|
2006
|
-
import * as
|
|
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
|
|
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(
|
|
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
|
|
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) && !
|
|
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 =
|
|
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
|
-
|
|
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({
|
|
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)
|
|
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(
|
|
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
|
|
2353
|
-
if (
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
[
|
|
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
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
|
2549
|
-
if (
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
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 "
|
|
3086
|
+
case "new": {
|
|
2746
3087
|
const text = args.join(" ");
|
|
2747
3088
|
if (!text) {
|
|
2748
|
-
setNotice("usage: /
|
|
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, {
|
|
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] ?
|
|
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(
|
|
3459
|
-
|
|
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 {
|