@ordewell/cli 0.5.4 → 0.5.5
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/main.js +1870 -1466
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/main.ts
|
|
27
|
-
var
|
|
27
|
+
var import_core30 = require("@ordewell/core");
|
|
28
28
|
|
|
29
29
|
// src/utils/env.ts
|
|
30
30
|
var import_fs = require("fs");
|
|
@@ -825,6 +825,11 @@ var ApiClient = class _ApiClient {
|
|
|
825
825
|
*/
|
|
826
826
|
streamPlanning(sessionId, onEvent) {
|
|
827
827
|
const socket = this.openSessionSocket(sessionId);
|
|
828
|
+
const ready = new Promise((resolve3) => {
|
|
829
|
+
socket.once("open", () => resolve3());
|
|
830
|
+
socket.once("error", () => resolve3());
|
|
831
|
+
socket.once("close", () => resolve3());
|
|
832
|
+
});
|
|
828
833
|
socket.on("message", (data) => {
|
|
829
834
|
try {
|
|
830
835
|
onEvent(JSON.parse(data.toString()));
|
|
@@ -833,7 +838,7 @@ var ApiClient = class _ApiClient {
|
|
|
833
838
|
});
|
|
834
839
|
socket.on("error", () => {
|
|
835
840
|
});
|
|
836
|
-
return { close: () => socket.close() };
|
|
841
|
+
return { ready, close: () => socket.close() };
|
|
837
842
|
}
|
|
838
843
|
async getCommands() {
|
|
839
844
|
const res = await this.httpRequest("GET", "/api/commands");
|
|
@@ -1043,7 +1048,7 @@ ${describeApproval(event)}
|
|
|
1043
1048
|
}
|
|
1044
1049
|
|
|
1045
1050
|
// src/commands/researchLog.ts
|
|
1046
|
-
var
|
|
1051
|
+
var import_core6 = require("@ordewell/core");
|
|
1047
1052
|
var OUTCOME_MARK = {
|
|
1048
1053
|
success: "\u2713",
|
|
1049
1054
|
failure: "\u2717",
|
|
@@ -1054,37 +1059,58 @@ var OUTCOME_MARK = {
|
|
|
1054
1059
|
var RESULT_CHARS = 160;
|
|
1055
1060
|
var VERBOSE_RESULT_CHARS = 600;
|
|
1056
1061
|
var THINKING_CHARS = 300;
|
|
1062
|
+
function summarize(tool, args, toolLabel) {
|
|
1063
|
+
const { name, keyArg } = (0, import_core6.toolHeadline)(tool, args, toolLabel);
|
|
1064
|
+
return `${name}(${keyArg})`;
|
|
1065
|
+
}
|
|
1057
1066
|
function oneLine(text, max) {
|
|
1058
1067
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1059
1068
|
return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
|
|
1060
1069
|
}
|
|
1061
1070
|
function formatStepLine(event, options = {}) {
|
|
1062
1071
|
if (event.type === "research_step") {
|
|
1063
|
-
const summary = (
|
|
1072
|
+
const summary = summarize(event.tool, event.args || "{}", event.toolLabel);
|
|
1064
1073
|
return event.subagentId ? ` \u21B3 ${summary}` : summary;
|
|
1065
1074
|
}
|
|
1066
1075
|
if (event.type === "research_step_done") {
|
|
1067
1076
|
const step2 = event.step;
|
|
1068
1077
|
if (!step2) return null;
|
|
1069
1078
|
const mark = OUTCOME_MARK[step2.outcome] ?? "\u2713";
|
|
1070
|
-
const summary = (
|
|
1079
|
+
const summary = summarize(step2.tool, step2.args, step2.toolLabel);
|
|
1071
1080
|
const result = oneLine(step2.result ?? "", options.verbose ? VERBOSE_RESULT_CHARS : RESULT_CHARS);
|
|
1072
1081
|
const indent = event.subagentId ? " \u21B3 " : "";
|
|
1073
1082
|
return result ? `${indent}${mark} ${summary} \u2192 ${result}` : `${indent}${mark} ${summary}`;
|
|
1074
1083
|
}
|
|
1075
|
-
if (event.type === "plan_thinking") {
|
|
1076
|
-
if (!options.verbose) return null;
|
|
1077
|
-
const text = oneLine(String(event.text ?? ""), THINKING_CHARS);
|
|
1078
|
-
return text ? ` \xB7 ${text}` : null;
|
|
1079
|
-
}
|
|
1080
1084
|
return null;
|
|
1081
1085
|
}
|
|
1082
1086
|
function isTransient(event) {
|
|
1083
1087
|
return event.type === "research_step";
|
|
1084
1088
|
}
|
|
1089
|
+
function createStepLog(options = {}) {
|
|
1090
|
+
let held = null;
|
|
1091
|
+
const flush = () => {
|
|
1092
|
+
const text = held ? oneLine(held.text, THINKING_CHARS) : "";
|
|
1093
|
+
held = null;
|
|
1094
|
+
return text ? [{ text: ` \xB7 ${text}`, transient: false }] : [];
|
|
1095
|
+
};
|
|
1096
|
+
return {
|
|
1097
|
+
push(event) {
|
|
1098
|
+
if (event.type === "planner_thinking_delta") {
|
|
1099
|
+
if (!options.verbose) return [];
|
|
1100
|
+
const thinker = `${event.turnId}|${event.segmentId}|${event.subagentId}`;
|
|
1101
|
+
const done = held && held.thinker !== thinker ? flush() : [];
|
|
1102
|
+
held = { thinker, text: (held?.text ?? "") + event.text };
|
|
1103
|
+
return done;
|
|
1104
|
+
}
|
|
1105
|
+
const line = formatStepLine(event, options);
|
|
1106
|
+
return line === null ? [] : [...flush(), { text: line, transient: isTransient(event) }];
|
|
1107
|
+
},
|
|
1108
|
+
flush
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1085
1111
|
|
|
1086
1112
|
// src/commands/plan.ts
|
|
1087
|
-
var
|
|
1113
|
+
var import_core7 = require("@ordewell/core");
|
|
1088
1114
|
function planTasks(plan) {
|
|
1089
1115
|
return plan ? allTasksOf(plan) : [];
|
|
1090
1116
|
}
|
|
@@ -1160,11 +1186,11 @@ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${man
|
|
|
1160
1186
|
for (const t of tasks) {
|
|
1161
1187
|
const typeIcon = t.type === "user" ? "[MAN]" : "[ AI]";
|
|
1162
1188
|
const deps = t.dependencies?.length ? ` \u2190 ${t.dependencies.join(", ")}` : "";
|
|
1163
|
-
console.log(` ${(0,
|
|
1189
|
+
console.log(` ${(0, import_core7.taskOrderLabel)(t).padStart(2)}. ${typeIcon} ${t.title}${modelSuffix(t)}${deps}`);
|
|
1164
1190
|
for (const sub of t.subtasks || []) {
|
|
1165
1191
|
const subTypeIcon = sub.type === "user" ? "[MAN]" : "[ AI]";
|
|
1166
1192
|
const subDeps = sub.dependencies?.length ? ` \u2190 ${sub.dependencies.join(", ")}` : "";
|
|
1167
|
-
console.log(` ${(0,
|
|
1193
|
+
console.log(` ${(0, import_core7.taskOrderLabel)(sub, t)}. ${subTypeIcon} ${sub.title}${modelSuffix(sub)}${subDeps}`);
|
|
1168
1194
|
}
|
|
1169
1195
|
}
|
|
1170
1196
|
if (manCount > 0) {
|
|
@@ -1211,25 +1237,31 @@ async function handlePlan(subArgs, deps = {}) {
|
|
|
1211
1237
|
runners2 = runnerFlags;
|
|
1212
1238
|
}
|
|
1213
1239
|
console.error(`Generating plan for: "${goal}"...`);
|
|
1214
|
-
const sessionId = (0,
|
|
1240
|
+
const sessionId = (0, import_core7.mintSessionId)();
|
|
1215
1241
|
const isTty = !!process.stderr.isTTY;
|
|
1216
1242
|
let lastLineLength = 0;
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
if (line === null) return;
|
|
1243
|
+
const stepLog = createStepLog({ verbose });
|
|
1244
|
+
function renderLine({ text, transient }) {
|
|
1220
1245
|
if (isTty) {
|
|
1221
|
-
process.stderr.write(`\r\x1B[2K${
|
|
1222
|
-
if (
|
|
1223
|
-
lastLineLength =
|
|
1246
|
+
process.stderr.write(`\r\x1B[2K${text}`);
|
|
1247
|
+
if (transient) {
|
|
1248
|
+
lastLineLength = text.length;
|
|
1224
1249
|
} else {
|
|
1225
1250
|
process.stderr.write("\n");
|
|
1226
1251
|
lastLineLength = 0;
|
|
1227
1252
|
}
|
|
1228
1253
|
} else {
|
|
1229
|
-
process.stderr.write(`${
|
|
1254
|
+
process.stderr.write(`${text}
|
|
1230
1255
|
`);
|
|
1231
1256
|
}
|
|
1232
1257
|
}
|
|
1258
|
+
function renderStep(event) {
|
|
1259
|
+
stepLog.push(event).forEach(renderLine);
|
|
1260
|
+
}
|
|
1261
|
+
function closeStream() {
|
|
1262
|
+
stream.close();
|
|
1263
|
+
stepLog.flush().forEach(renderLine);
|
|
1264
|
+
}
|
|
1233
1265
|
let reader = deps.reader;
|
|
1234
1266
|
const handleApproval = createApprovalHandler(sessionId, {
|
|
1235
1267
|
respond: (sessionId2, approvalId, granted) => api.respondToApproval(sessionId2, approvalId, granted),
|
|
@@ -1246,6 +1278,7 @@ async function handlePlan(subArgs, deps = {}) {
|
|
|
1246
1278
|
void handleApproval(event);
|
|
1247
1279
|
});
|
|
1248
1280
|
try {
|
|
1281
|
+
await stream.ready;
|
|
1249
1282
|
let plan;
|
|
1250
1283
|
let models = [];
|
|
1251
1284
|
if (oneShot) {
|
|
@@ -1261,6 +1294,7 @@ async function handlePlan(subArgs, deps = {}) {
|
|
|
1261
1294
|
() => api.startConversation(sessionId, goal, runners2, workspace)
|
|
1262
1295
|
);
|
|
1263
1296
|
while (!hasCommittedPlan(plan)) {
|
|
1297
|
+
stepLog.flush().forEach(renderLine);
|
|
1264
1298
|
console.log(`
|
|
1265
1299
|
${lastPlannerMessage(plan)}
|
|
1266
1300
|
`);
|
|
@@ -1286,7 +1320,7 @@ ${lastPlannerMessage(plan)}
|
|
|
1286
1320
|
}
|
|
1287
1321
|
reader?.close();
|
|
1288
1322
|
}
|
|
1289
|
-
|
|
1323
|
+
closeStream();
|
|
1290
1324
|
if (isTty && lastLineLength > 0) process.stderr.write("\r\x1B[2K");
|
|
1291
1325
|
process.stderr.write("\n");
|
|
1292
1326
|
printPlan(plan, sessionId, runners2, models);
|
|
@@ -1295,7 +1329,7 @@ ${lastPlannerMessage(plan)}
|
|
|
1295
1329
|
Run 'ordewell run' to execute, 'ordewell status' to inspect, or 'ordewell tui' for the full UI.`);
|
|
1296
1330
|
} catch (err) {
|
|
1297
1331
|
reader?.close();
|
|
1298
|
-
|
|
1332
|
+
closeStream();
|
|
1299
1333
|
if (isTty && lastLineLength > 0) process.stderr.write("\r\x1B[2K");
|
|
1300
1334
|
process.stderr.write("\n");
|
|
1301
1335
|
console.error(`Plan generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1304,7 +1338,7 @@ ${lastPlannerMessage(plan)}
|
|
|
1304
1338
|
}
|
|
1305
1339
|
|
|
1306
1340
|
// src/isolation.ts
|
|
1307
|
-
var
|
|
1341
|
+
var import_core8 = require("@ordewell/core");
|
|
1308
1342
|
var STATE_OF = {
|
|
1309
1343
|
active: "active",
|
|
1310
1344
|
merged: "integrated",
|
|
@@ -1388,7 +1422,7 @@ function repairedLanded(handoff) {
|
|
|
1388
1422
|
function repairedNotice(handoff) {
|
|
1389
1423
|
const repaired = repairedLanded(handoff);
|
|
1390
1424
|
if (repaired.length === 0) return "";
|
|
1391
|
-
const named = repaired.map((t) => `${t.title} (${(0,
|
|
1425
|
+
const named = repaired.map((t) => `${t.title} (${(0, import_core8.capConflictFiles)(t.repairedFiles)})`);
|
|
1392
1426
|
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
1393
1427
|
return ` ${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
1394
1428
|
}
|
|
@@ -1406,7 +1440,7 @@ function mergeOutcome(result, branch, group, repaired = []) {
|
|
|
1406
1440
|
if (!group && result.outcome === "conflict") {
|
|
1407
1441
|
return { ok, message: `Merging ${branch} conflicted, so it was aborted \u2014 your tree is as it was. Merge it with git and resolve the conflict there.` };
|
|
1408
1442
|
}
|
|
1409
|
-
const { message } = (0,
|
|
1443
|
+
const { message } = (0, import_core8.describeMergeResult)(result, branch, group, repaired);
|
|
1410
1444
|
return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
|
|
1411
1445
|
}
|
|
1412
1446
|
|
|
@@ -1427,24 +1461,23 @@ function iconFor(status) {
|
|
|
1427
1461
|
}
|
|
1428
1462
|
|
|
1429
1463
|
// src/commands/run.ts
|
|
1430
|
-
var
|
|
1464
|
+
var import_core9 = require("@ordewell/core");
|
|
1431
1465
|
|
|
1432
1466
|
// src/catalog.ts
|
|
1433
1467
|
function normalizeCatalog(result) {
|
|
1434
1468
|
const runnersByModel = /* @__PURE__ */ new Map();
|
|
1435
1469
|
for (const [runner, entries] of Object.entries(result.modelsByRunner ?? {})) {
|
|
1436
1470
|
for (const entry of entries ?? []) {
|
|
1437
|
-
const id = String(entry.modelId
|
|
1471
|
+
const id = String(entry.modelId);
|
|
1438
1472
|
runnersByModel.set(id, [.../* @__PURE__ */ new Set([...runnersByModel.get(id) ?? [], runner])]);
|
|
1439
1473
|
}
|
|
1440
1474
|
}
|
|
1441
1475
|
const models = (result.models ?? []).map((m) => {
|
|
1442
|
-
const id = String(m.modelId
|
|
1476
|
+
const id = String(m.modelId);
|
|
1443
1477
|
return {
|
|
1444
1478
|
id,
|
|
1445
|
-
label: String(m.modelLabel ??
|
|
1446
|
-
provider: String(m.runnerProviderLabel ?? m.runnerProvider ??
|
|
1447
|
-
pricing: m.pricing ? `$${m.pricing}/MTok` : void 0,
|
|
1479
|
+
label: String(m.modelLabel ?? id),
|
|
1480
|
+
provider: String(m.runnerProviderLabel ?? m.runnerProvider ?? ""),
|
|
1448
1481
|
variants: Array.isArray(m.variants) ? m.variants.map((v) => ({ id: String(v.id), label: String(v.label ?? v.id) })) : [],
|
|
1449
1482
|
runners: runnersByModel.get(id) ?? []
|
|
1450
1483
|
};
|
|
@@ -1564,11 +1597,11 @@ async function followExecution(api, sessionId, start, onBlocked) {
|
|
|
1564
1597
|
});
|
|
1565
1598
|
const stream = api.streamExecution(sessionId, (event) => {
|
|
1566
1599
|
if (event.type === "task_started") {
|
|
1567
|
-
process.stderr.write(
|
|
1600
|
+
process.stderr.write(`\xB7 ${(0, import_core9.taskStartedNotice)(event.title, event.runner)}
|
|
1568
1601
|
`);
|
|
1569
1602
|
}
|
|
1570
1603
|
if (event.type === "checkpoint") {
|
|
1571
|
-
process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0,
|
|
1604
|
+
process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core9.truncateCheckpointSummary)(event.summary)}
|
|
1572
1605
|
`);
|
|
1573
1606
|
}
|
|
1574
1607
|
if (event.type === "status_update" && event.tasks) {
|
|
@@ -1637,7 +1670,7 @@ async function handleRun(subArgs, injectedApi) {
|
|
|
1637
1670
|
|
|
1638
1671
|
// src/commands/status.ts
|
|
1639
1672
|
var import_fs4 = require("fs");
|
|
1640
|
-
var
|
|
1673
|
+
var import_core10 = require("@ordewell/core");
|
|
1641
1674
|
async function handleStatus(subArgs, injectedApi) {
|
|
1642
1675
|
const sessionId = flag(subArgs, "--session-id");
|
|
1643
1676
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
@@ -1708,12 +1741,12 @@ function printOneSession(sessionId, meta, plan) {
|
|
|
1708
1741
|
const icon = iconFor(t.status);
|
|
1709
1742
|
const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
|
|
1710
1743
|
const shortId = (t.id ?? "").slice(-8);
|
|
1711
|
-
console.log(` ${(0,
|
|
1744
|
+
console.log(` ${(0, import_core10.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
|
|
1712
1745
|
for (const sub of t.subtasks || []) {
|
|
1713
1746
|
const subIcon = iconFor(sub.status);
|
|
1714
1747
|
const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
|
|
1715
1748
|
const subShortId = (sub.id ?? "").slice(-8);
|
|
1716
|
-
console.log(` ${(0,
|
|
1749
|
+
console.log(` ${(0, import_core10.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
|
|
1717
1750
|
}
|
|
1718
1751
|
}
|
|
1719
1752
|
}
|
|
@@ -1765,10 +1798,10 @@ async function handleWeb(subArgs) {
|
|
|
1765
1798
|
}
|
|
1766
1799
|
|
|
1767
1800
|
// src/commands/models.ts
|
|
1768
|
-
var
|
|
1801
|
+
var import_core12 = require("@ordewell/core");
|
|
1769
1802
|
|
|
1770
1803
|
// src/commands/model.ts
|
|
1771
|
-
var
|
|
1804
|
+
var import_core11 = require("@ordewell/core");
|
|
1772
1805
|
var USAGE = "Usage: ordewell model [show | set <model-id>]";
|
|
1773
1806
|
async function handleModel(subArgs, injectedApi) {
|
|
1774
1807
|
const [action, modelId] = positionals(subArgs);
|
|
@@ -1783,7 +1816,7 @@ async function handleModel(subArgs, injectedApi) {
|
|
|
1783
1816
|
if (!modelId) fail(USAGE);
|
|
1784
1817
|
const settings = await api.getSettings();
|
|
1785
1818
|
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1786
|
-
const runner = (0,
|
|
1819
|
+
const runner = (0, import_core11.runnerForProvider)(provider);
|
|
1787
1820
|
if (runner) {
|
|
1788
1821
|
const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
|
|
1789
1822
|
if (known.length > 0 && !known.some((m) => m.id === modelId)) {
|
|
@@ -1802,7 +1835,7 @@ async function showModels(api) {
|
|
|
1802
1835
|
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
1803
1836
|
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1804
1837
|
const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
1805
|
-
const runner = (0,
|
|
1838
|
+
const runner = (0, import_core11.runnerForProvider)(provider);
|
|
1806
1839
|
const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
|
|
1807
1840
|
console.log(`
|
|
1808
1841
|
Planner model: ${current || "(backend default)"}`);
|
|
@@ -1836,8 +1869,8 @@ async function handleModels(subArgs) {
|
|
|
1836
1869
|
const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
|
|
1837
1870
|
return handleModel(["set", subArgs[setIdx + 1], ...rest]);
|
|
1838
1871
|
}
|
|
1839
|
-
const config = new
|
|
1840
|
-
const providers = (0,
|
|
1872
|
+
const config = new import_core12.EnvConfig();
|
|
1873
|
+
const providers = (0, import_core12.configuredProviders)(config);
|
|
1841
1874
|
if (providers.length === 0) {
|
|
1842
1875
|
console.error("No providers configured. Set an API key for a provider, e.g.:");
|
|
1843
1876
|
console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
|
|
@@ -1846,19 +1879,19 @@ async function handleModels(subArgs) {
|
|
|
1846
1879
|
console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
|
|
1847
1880
|
process.exit(1);
|
|
1848
1881
|
}
|
|
1849
|
-
const { apiKeys, baseUrls } = (0,
|
|
1882
|
+
const { apiKeys, baseUrls } = (0, import_core12.collectProviderCredentials)(config);
|
|
1850
1883
|
let models;
|
|
1851
1884
|
let errors;
|
|
1852
1885
|
try {
|
|
1853
|
-
const result = await (0,
|
|
1854
|
-
models = (0,
|
|
1886
|
+
const result = await (0, import_core12.fetchAllProviderModels)({ apiKeys, baseUrls });
|
|
1887
|
+
models = (0, import_core12.toOrchestratorOptions)(result.models, import_core12.ORCHESTRATOR_SHORTCUTS);
|
|
1855
1888
|
errors = result.errors;
|
|
1856
1889
|
} catch (err) {
|
|
1857
1890
|
console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
|
|
1858
1891
|
process.exit(1);
|
|
1859
1892
|
return;
|
|
1860
1893
|
}
|
|
1861
|
-
const providerLabel = (p) => (0,
|
|
1894
|
+
const providerLabel = (p) => (0, import_core12.getProviderMeta)(p)?.label ?? p;
|
|
1862
1895
|
console.log(
|
|
1863
1896
|
`
|
|
1864
1897
|
Available models (${models.length}) across ${providers.length} configured provider(s):`
|
|
@@ -1975,9 +2008,9 @@ function handleSetup(subArgs) {
|
|
|
1975
2008
|
}
|
|
1976
2009
|
|
|
1977
2010
|
// src/commands/plugins.ts
|
|
1978
|
-
var
|
|
2011
|
+
var import_core13 = require("@ordewell/core");
|
|
1979
2012
|
function handlePlugins(subArgs) {
|
|
1980
|
-
const registry = new
|
|
2013
|
+
const registry = new import_core13.RunnerRegistry();
|
|
1981
2014
|
registry.loadUserPlugins();
|
|
1982
2015
|
const sub = subArgs[0];
|
|
1983
2016
|
if (sub === "list" || !sub) {
|
|
@@ -2013,7 +2046,7 @@ ${plugins.length} plugin(s):
|
|
|
2013
2046
|
process.exit(1);
|
|
2014
2047
|
}
|
|
2015
2048
|
try {
|
|
2016
|
-
const resolved = (0,
|
|
2049
|
+
const resolved = (0, import_core13.classifyPluginSource)(source);
|
|
2017
2050
|
const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
|
|
2018
2051
|
console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
|
|
2019
2052
|
} catch (err) {
|
|
@@ -2115,7 +2148,7 @@ async function rejectStrayIds(runner, ids, api) {
|
|
|
2115
2148
|
let discovered = [];
|
|
2116
2149
|
try {
|
|
2117
2150
|
const { modelsByRunner } = await api.getModels();
|
|
2118
|
-
discovered = (modelsByRunner?.[runner] ?? []).map((m) => String(m.modelId
|
|
2151
|
+
discovered = (modelsByRunner?.[runner] ?? []).map((m) => String(m.modelId));
|
|
2119
2152
|
} catch {
|
|
2120
2153
|
return;
|
|
2121
2154
|
}
|
|
@@ -2481,6 +2514,30 @@ async function handleRewind(subArgs, injectedApi) {
|
|
|
2481
2514
|
// src/commands/handoff.ts
|
|
2482
2515
|
var import_readline2 = require("readline");
|
|
2483
2516
|
|
|
2517
|
+
// src/tui/transcript.ts
|
|
2518
|
+
var import_core14 = require("@ordewell/core");
|
|
2519
|
+
function show(state, { view: conversation, gate: turnGate }) {
|
|
2520
|
+
const gated = turnGate === state.turnGate ? state : { ...state, turnGate };
|
|
2521
|
+
if (conversation === state.conversation) return gated;
|
|
2522
|
+
const scroll = conversation.nextId > state.conversation.nextId ? 0 : state.scroll;
|
|
2523
|
+
return { ...gated, conversation, scroll };
|
|
2524
|
+
}
|
|
2525
|
+
function fold(state, input) {
|
|
2526
|
+
return show(state, { view: (0, import_core14.reduceConversation)(state.conversation, input), gate: state.turnGate });
|
|
2527
|
+
}
|
|
2528
|
+
function say(state, role, text) {
|
|
2529
|
+
return fold(state, { type: "local_entry", role, text });
|
|
2530
|
+
}
|
|
2531
|
+
function hear(state, message) {
|
|
2532
|
+
return show(state, (0, import_core14.followTurn)(state.conversation, state.turnGate, message));
|
|
2533
|
+
}
|
|
2534
|
+
function cutTurn(state) {
|
|
2535
|
+
return show(state, (0, import_core14.stopTurn)(state.conversation, state.turnGate));
|
|
2536
|
+
}
|
|
2537
|
+
function wiped(view) {
|
|
2538
|
+
return { ...view, blocks: view.blocks.filter((b) => b.type === "usage") };
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2484
2541
|
// src/tui/ansi.ts
|
|
2485
2542
|
var import_string_width = __toESM(require("string-width"));
|
|
2486
2543
|
var ESCAPE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\|$)|[P^_][\s\S]*?(?:\x1b\\|$)|\[[0-?]*[ -/]*[@-~]?|[ -/]*[0-~]?)/g;
|
|
@@ -2623,18 +2680,6 @@ var style = {
|
|
|
2623
2680
|
grey: colour("90")
|
|
2624
2681
|
};
|
|
2625
2682
|
|
|
2626
|
-
// src/tui/transcript.ts
|
|
2627
|
-
function say(state, role, content, research, extra) {
|
|
2628
|
-
const message = {
|
|
2629
|
-
role,
|
|
2630
|
-
content: sanitize(content),
|
|
2631
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2632
|
-
...research ? { research } : {},
|
|
2633
|
-
...extra
|
|
2634
|
-
};
|
|
2635
|
-
return { ...state, messages: [...state.messages, message], scroll: 0 };
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
2683
|
// src/tui/handoff.ts
|
|
2639
2684
|
var HANDOFF_ACTIONS = [
|
|
2640
2685
|
{ id: "review", label: "Review diff", hint: "the branch against the commit it forked from" },
|
|
@@ -2882,7 +2927,7 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
|
2882
2927
|
}
|
|
2883
2928
|
|
|
2884
2929
|
// src/commands/planner.ts
|
|
2885
|
-
var
|
|
2930
|
+
var import_core15 = require("@ordewell/core");
|
|
2886
2931
|
|
|
2887
2932
|
// src/plannerModelSwitch.ts
|
|
2888
2933
|
function describePlannerSwitch(settings, provider) {
|
|
@@ -2901,7 +2946,7 @@ function rememberedModelFor(settings, provider) {
|
|
|
2901
2946
|
}
|
|
2902
2947
|
|
|
2903
2948
|
// src/commands/planner.ts
|
|
2904
|
-
var KNOWN_PROVIDERS = Object.keys(
|
|
2949
|
+
var KNOWN_PROVIDERS = Object.keys(import_core15.ALL_PROVIDERS);
|
|
2905
2950
|
async function handlePlanner(subArgs, injectedApi) {
|
|
2906
2951
|
const api = await connect(subArgs, injectedApi);
|
|
2907
2952
|
const [wanted] = positionals(subArgs);
|
|
@@ -2916,13 +2961,13 @@ async function handlePlanner(subArgs, injectedApi) {
|
|
|
2916
2961
|
const settings = await persistEnv(api, { AI_PROVIDER: provider });
|
|
2917
2962
|
const recall2 = describePlannerSwitch(settings, provider);
|
|
2918
2963
|
writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
|
|
2919
|
-
const meta =
|
|
2964
|
+
const meta = import_core15.ALL_PROVIDERS[provider];
|
|
2920
2965
|
const catalog = await fetchCatalog(api);
|
|
2921
2966
|
console.log(
|
|
2922
|
-
(0,
|
|
2967
|
+
(0, import_core15.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
|
|
2923
2968
|
);
|
|
2924
2969
|
console.log(plannerModelLine(recall2));
|
|
2925
|
-
if (!(0,
|
|
2970
|
+
if (!(0, import_core15.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
|
|
2926
2971
|
console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
|
|
2927
2972
|
}
|
|
2928
2973
|
}
|
|
@@ -2940,15 +2985,15 @@ async function showPlanners(api) {
|
|
|
2940
2985
|
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
2941
2986
|
const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
2942
2987
|
console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
|
|
2943
|
-
for (const id of
|
|
2944
|
-
const runner = (0,
|
|
2988
|
+
for (const id of import_core15.CLI_PROVIDERS) {
|
|
2989
|
+
const runner = (0, import_core15.runnerForProvider)(id);
|
|
2945
2990
|
const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
|
|
2946
|
-
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${
|
|
2991
|
+
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core15.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
|
|
2947
2992
|
}
|
|
2948
2993
|
console.log("\nAPI providers");
|
|
2949
|
-
for (const id of
|
|
2994
|
+
for (const id of import_core15.PROVIDER_PRIORITY) {
|
|
2950
2995
|
const configured = catalog.providers.includes(id);
|
|
2951
|
-
const meta =
|
|
2996
|
+
const meta = import_core15.ALL_PROVIDERS[id];
|
|
2952
2997
|
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
|
|
2953
2998
|
}
|
|
2954
2999
|
console.log(`
|
|
@@ -2965,7 +3010,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
|
|
|
2965
3010
|
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
2966
3011
|
const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
2967
3012
|
const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
|
|
2968
|
-
const runner = (0,
|
|
3013
|
+
const runner = (0, import_core15.runnerForProvider)(provider);
|
|
2969
3014
|
if (!runner) {
|
|
2970
3015
|
fail(
|
|
2971
3016
|
"Thinking effort applies to a coding-agent planner.",
|
|
@@ -3005,7 +3050,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
|
|
|
3005
3050
|
}
|
|
3006
3051
|
|
|
3007
3052
|
// src/commands/parallel.ts
|
|
3008
|
-
var
|
|
3053
|
+
var import_core16 = require("@ordewell/core");
|
|
3009
3054
|
var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
|
|
3010
3055
|
async function handleParallel(subArgs, injectedApi) {
|
|
3011
3056
|
const [wanted] = positionals(subArgs);
|
|
@@ -3015,14 +3060,14 @@ async function handleParallel(subArgs, injectedApi) {
|
|
|
3015
3060
|
console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
|
|
3016
3061
|
return;
|
|
3017
3062
|
}
|
|
3018
|
-
const limit = (0,
|
|
3063
|
+
const limit = (0, import_core16.parseMaxParallel)(wanted);
|
|
3019
3064
|
if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
|
|
3020
3065
|
await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
|
|
3021
3066
|
console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
|
|
3022
3067
|
}
|
|
3023
3068
|
|
|
3024
3069
|
// src/commands/key.ts
|
|
3025
|
-
var
|
|
3070
|
+
var import_core17 = require("@ordewell/core");
|
|
3026
3071
|
var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
|
|
3027
3072
|
async function handleKey(subArgs, injectedApi) {
|
|
3028
3073
|
const [action, provider, ...keyParts] = positionals(subArgs);
|
|
@@ -3033,8 +3078,8 @@ async function handleKey(subArgs, injectedApi) {
|
|
|
3033
3078
|
if (action !== "set") {
|
|
3034
3079
|
const catalog = await fetchCatalog(api);
|
|
3035
3080
|
console.log("\nAPI provider keys\n");
|
|
3036
|
-
for (const id2 of
|
|
3037
|
-
const meta2 =
|
|
3081
|
+
for (const id2 of import_core17.PROVIDER_PRIORITY) {
|
|
3082
|
+
const meta2 = import_core17.ALL_PROVIDERS[id2];
|
|
3038
3083
|
const configured = catalog.providers.includes(id2);
|
|
3039
3084
|
console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
|
|
3040
3085
|
}
|
|
@@ -3044,7 +3089,7 @@ async function handleKey(subArgs, injectedApi) {
|
|
|
3044
3089
|
}
|
|
3045
3090
|
if (!provider) fail(USAGE6);
|
|
3046
3091
|
const id = provider.toLowerCase();
|
|
3047
|
-
const meta =
|
|
3092
|
+
const meta = import_core17.ALL_PROVIDERS[id];
|
|
3048
3093
|
if (!meta) {
|
|
3049
3094
|
fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
|
|
3050
3095
|
}
|
|
@@ -3125,7 +3170,7 @@ async function handleApprove(subArgs, injectedApi) {
|
|
|
3125
3170
|
// src/tui/terminalLauncher.ts
|
|
3126
3171
|
var import_child_process3 = require("child_process");
|
|
3127
3172
|
var import_util = require("util");
|
|
3128
|
-
var
|
|
3173
|
+
var import_core18 = require("@ordewell/core");
|
|
3129
3174
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
3130
3175
|
var defaultExecFile = async (command, args) => {
|
|
3131
3176
|
const { stdout, stderr } = await execFileAsync(command, args);
|
|
@@ -3145,16 +3190,16 @@ var defaultWhich = (bin) => {
|
|
|
3145
3190
|
async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
|
|
3146
3191
|
const platform = deps.platform ?? process.platform;
|
|
3147
3192
|
const env = deps.env ?? process.env;
|
|
3148
|
-
const hasTmuxImpl = deps.hasTmuxImpl ??
|
|
3193
|
+
const hasTmuxImpl = deps.hasTmuxImpl ?? import_core18.hasTmux;
|
|
3149
3194
|
const execFileImpl = deps.execFileImpl ?? defaultExecFile;
|
|
3150
3195
|
const spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
3151
3196
|
const which = deps.which ?? defaultWhich;
|
|
3152
3197
|
if (!hasTmuxImpl()) {
|
|
3153
3198
|
return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
|
|
3154
3199
|
}
|
|
3155
|
-
const session = (0,
|
|
3156
|
-
const socket = (0,
|
|
3157
|
-
const base = (0,
|
|
3200
|
+
const session = (0, import_core18.tmuxSessionName)(port);
|
|
3201
|
+
const socket = (0, import_core18.tmuxSocketName)(port);
|
|
3202
|
+
const base = (0, import_core18.tmuxWindowName)(taskId, planSessionId);
|
|
3158
3203
|
let windows;
|
|
3159
3204
|
try {
|
|
3160
3205
|
const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
|
|
@@ -3246,7 +3291,7 @@ async function handleTerminal(subArgs, injectedApi) {
|
|
|
3246
3291
|
}
|
|
3247
3292
|
|
|
3248
3293
|
// src/commands/task-assign.ts
|
|
3249
|
-
var
|
|
3294
|
+
var import_plan_utils = require("@ordewell/core/plan-utils");
|
|
3250
3295
|
|
|
3251
3296
|
// src/tui/taskAssignment.ts
|
|
3252
3297
|
function runnerServes(runner, model) {
|
|
@@ -3423,7 +3468,7 @@ Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assigned
|
|
|
3423
3468
|
var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
|
|
3424
3469
|
async function handleTaskDeps(subArgs, injectedApi) {
|
|
3425
3470
|
await withTask(subArgs, DEPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
|
|
3426
|
-
const candidates = (0,
|
|
3471
|
+
const candidates = (0, import_plan_utils.dependencyCandidates)(tasks, task.id);
|
|
3427
3472
|
if (!value) {
|
|
3428
3473
|
console.log(`
|
|
3429
3474
|
Dependencies \xB7 #${task.order} ${task.title}
|
|
@@ -3440,7 +3485,7 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
3440
3485
|
return;
|
|
3441
3486
|
}
|
|
3442
3487
|
const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) => tasks.find((t) => t.id === token || String(t.order) === token)?.id ?? token);
|
|
3443
|
-
const check = (0,
|
|
3488
|
+
const check = (0, import_plan_utils.canSetDependencies)(tasks, task.id, dependencies);
|
|
3444
3489
|
if (!check.ok) fail(check.error ?? "Invalid dependencies.");
|
|
3445
3490
|
await api.updateTask(sessionId, task.id, { dependencies });
|
|
3446
3491
|
console.log(dependencies.length > 0 ? `Task #${task.order} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task #${task.order} no longer depends on anything.`);
|
|
@@ -3448,7 +3493,7 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
3448
3493
|
}
|
|
3449
3494
|
|
|
3450
3495
|
// src/tui/index.ts
|
|
3451
|
-
var
|
|
3496
|
+
var import_core29 = require("@ordewell/core");
|
|
3452
3497
|
|
|
3453
3498
|
// src/tui/editor.ts
|
|
3454
3499
|
function emptyEditor() {
|
|
@@ -3693,6 +3738,9 @@ function tokenCompletions(token) {
|
|
|
3693
3738
|
return pool.filter((c) => c.name.startsWith(token.name));
|
|
3694
3739
|
}
|
|
3695
3740
|
|
|
3741
|
+
// src/tui/blocks.ts
|
|
3742
|
+
var import_core19 = require("@ordewell/core");
|
|
3743
|
+
|
|
3696
3744
|
// src/tui/markdown.ts
|
|
3697
3745
|
function renderMarkdown(source, cols) {
|
|
3698
3746
|
const lines = source.replace(/\r/g, "").replace(/^\n+|\n+$/g, "").split("\n");
|
|
@@ -3804,6 +3852,227 @@ function inline(text) {
|
|
|
3804
3852
|
return text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+[^)]*)?\)/g, "$1 ($2)").replace(/\*\*([^*]+)\*\*/g, (_, bold) => style.bold(bold)).replace(/__([^_]+)__/g, (_, bold) => style.bold(bold)).replace(/(^|[^\w])\*([^*]+)\*(?![\w*])/g, (_, prefix, italic) => `${prefix}${style.italic(italic)}`).replace(/(^|[^\w])_([^_]+)_(?![\w_])/g, (_, prefix, italic) => `${prefix}${style.italic(italic)}`).replace(/`([^`]+)`/g, (_, code) => style.cyan(code));
|
|
3805
3853
|
}
|
|
3806
3854
|
|
|
3855
|
+
// src/tui/blocks.ts
|
|
3856
|
+
function countOf(n, noun) {
|
|
3857
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
3858
|
+
}
|
|
3859
|
+
function firstLine(text) {
|
|
3860
|
+
return sanitize(text).trim().split("\n")[0].trim();
|
|
3861
|
+
}
|
|
3862
|
+
function tailOf(text, max) {
|
|
3863
|
+
const chars = [...text];
|
|
3864
|
+
let out = "";
|
|
3865
|
+
let used = 0;
|
|
3866
|
+
for (let i = chars.length - 1; i >= 0 && used + width(chars[i]) <= max; i--) {
|
|
3867
|
+
out = chars[i] + out;
|
|
3868
|
+
used += width(chars[i]);
|
|
3869
|
+
}
|
|
3870
|
+
return out;
|
|
3871
|
+
}
|
|
3872
|
+
var ROLE_PREFIX = {
|
|
3873
|
+
user: (t) => `${style.cyan("\u276F")} ${t}`,
|
|
3874
|
+
planner: (t) => `${style.magenta("\u25C6")} ${t}`,
|
|
3875
|
+
system: (t) => style.grey(`\xB7 ${t}`),
|
|
3876
|
+
error: (t) => `${style.red("\u2717")} ${style.red(t)}`
|
|
3877
|
+
};
|
|
3878
|
+
function messageLines(block, cols) {
|
|
3879
|
+
const text = sanitize(block.text);
|
|
3880
|
+
const room = Math.max(1, cols - 2);
|
|
3881
|
+
const wrapped = block.role === "planner" ? renderMarkdown(text, room) : wrap(text, room);
|
|
3882
|
+
return wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[block.role](line) : ` ${line}`);
|
|
3883
|
+
}
|
|
3884
|
+
function thinkingLines(block, cols, detailAll) {
|
|
3885
|
+
const text = sanitize(block.text).trim();
|
|
3886
|
+
const head = `\u2234 Thinking (${countOf(text ? text.split(/\s+/).length : 0, "word")})`;
|
|
3887
|
+
if (detailAll) {
|
|
3888
|
+
const body = text ? wrap(text, Math.max(1, cols - 2)).map((line) => style.grey(` ${line}`)) : [];
|
|
3889
|
+
return [style.grey(truncate(head, cols)), ...body];
|
|
3890
|
+
}
|
|
3891
|
+
const latest = block.streaming ? (text.split("\n").at(-1) ?? "").trim() : "";
|
|
3892
|
+
const room = cols - width(`${head} \xB7 `);
|
|
3893
|
+
if (!latest || room < 2) return [style.grey(truncate(head, cols))];
|
|
3894
|
+
const shown = width(latest) > room ? `\u2026${tailOf(latest, room - 1)}` : latest;
|
|
3895
|
+
return [style.grey(`${head} \xB7 ${shown}`)];
|
|
3896
|
+
}
|
|
3897
|
+
var OUTPUT_HEAD = " \u23BF ";
|
|
3898
|
+
var OUTPUT_INDENT = " ";
|
|
3899
|
+
var PREVIEW_LINES = 3;
|
|
3900
|
+
function underHeader(lines) {
|
|
3901
|
+
return lines.map((line, i) => `${i === 0 ? style.grey(OUTPUT_HEAD) : OUTPUT_INDENT}${line}`);
|
|
3902
|
+
}
|
|
3903
|
+
function unsettledOutcome(block) {
|
|
3904
|
+
switch (block.status) {
|
|
3905
|
+
case "error":
|
|
3906
|
+
return { label: "Error", paint: style.red };
|
|
3907
|
+
case "denied":
|
|
3908
|
+
return { label: block.outcome === "refused" ? "Refused" : "Denied", paint: style.yellow };
|
|
3909
|
+
case "interrupted":
|
|
3910
|
+
return { label: "Interrupted", paint: style.grey };
|
|
3911
|
+
default:
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
function commandMark(block) {
|
|
3916
|
+
if (block.status === "pending") return style.grey("\u25CB");
|
|
3917
|
+
return (unsettledOutcome(block)?.paint ?? style.green)("\u25CF");
|
|
3918
|
+
}
|
|
3919
|
+
function commandHeader(block, cols, detailAll) {
|
|
3920
|
+
const name = sanitize(block.headline.name);
|
|
3921
|
+
const keyArg = sanitize(block.headline.keyArg);
|
|
3922
|
+
if (detailAll) {
|
|
3923
|
+
return wrap(`${name}(${keyArg})`, Math.max(1, cols - 2)).map((line, i) => i === 0 ? `${commandMark(block)} ${line}` : ` ${line}`);
|
|
3924
|
+
}
|
|
3925
|
+
const open = `\u25CF ${name}(`;
|
|
3926
|
+
const room = cols - width(open) - 1;
|
|
3927
|
+
const text = room > 1 ? `${open}${truncate(keyArg, room)})` : truncate(`${open}${keyArg})`, cols);
|
|
3928
|
+
return [`${commandMark(block)}${text.slice(1)}`];
|
|
3929
|
+
}
|
|
3930
|
+
function argumentLines(args) {
|
|
3931
|
+
if (!args.trim()) return [];
|
|
3932
|
+
let parsed;
|
|
3933
|
+
try {
|
|
3934
|
+
parsed = JSON.parse(args);
|
|
3935
|
+
} catch {
|
|
3936
|
+
return sanitize(args).split("\n");
|
|
3937
|
+
}
|
|
3938
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return sanitize(args).split("\n");
|
|
3939
|
+
return Object.entries(parsed).flatMap(([key, value]) => {
|
|
3940
|
+
const [first, ...rest] = sanitize(typeof value === "string" ? value : JSON.stringify(value)).split("\n");
|
|
3941
|
+
return [`${key}: ${first}`, ...rest];
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
function resultLines(block, room, detailAll) {
|
|
3945
|
+
if (block.status === "pending") return [style.grey("Running\u2026")];
|
|
3946
|
+
const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
|
|
3947
|
+
const outcome = unsettledOutcome(block);
|
|
3948
|
+
if (outcome) {
|
|
3949
|
+
const [reason, ...rest] = (0, import_core19.outputLines)(block.output).map(sanitize);
|
|
3950
|
+
const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
|
|
3951
|
+
return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
|
|
3952
|
+
}
|
|
3953
|
+
if (detailAll) {
|
|
3954
|
+
const lines2 = (0, import_core19.outputLines)(block.output).map(sanitize);
|
|
3955
|
+
const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
|
|
3956
|
+
return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
|
|
3957
|
+
}
|
|
3958
|
+
const { lines, hiddenLineCount } = (0, import_core19.outputPreview)(block.output, PREVIEW_LINES);
|
|
3959
|
+
if (lines.length === 0) return [style.grey("(no output)")];
|
|
3960
|
+
const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
|
|
3961
|
+
return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
|
|
3962
|
+
}
|
|
3963
|
+
function toolLines(block, cols, detailAll) {
|
|
3964
|
+
const room = Math.max(1, cols - OUTPUT_INDENT.length);
|
|
3965
|
+
const args = detailAll ? argumentLines(block.args).flatMap((line) => wrap(line, room)).map((line) => OUTPUT_INDENT + style.grey(line)) : [];
|
|
3966
|
+
return [...commandHeader(block, cols, detailAll), ...args, ...underHeader(resultLines(block, room, detailAll))];
|
|
3967
|
+
}
|
|
3968
|
+
var SUBAGENT_PAINT = {
|
|
3969
|
+
running: style.yellow,
|
|
3970
|
+
done: style.green,
|
|
3971
|
+
failed: style.red,
|
|
3972
|
+
stopped: style.grey
|
|
3973
|
+
};
|
|
3974
|
+
var CHILD_INDENT = " ";
|
|
3975
|
+
function subagentHeader(block, cols) {
|
|
3976
|
+
const brief = firstLine(block.brief);
|
|
3977
|
+
const steps = block.children.filter((c) => c.type === "tool").length;
|
|
3978
|
+
const status = block.status === "running" && steps > 0 ? `running \xB7 ${countOf(steps, "step")}` : block.status;
|
|
3979
|
+
const head = brief ? " Agent: " : " Agent";
|
|
3980
|
+
const briefRoom = cols - width(`\u25C6${head} ${status}`);
|
|
3981
|
+
if (briefRoom < 4) return truncate(`\u25C6${head}${brief} ${status}`, cols);
|
|
3982
|
+
const named = `${style.cyan("\u25C6")}${head}${truncate(brief, briefRoom)} ${SUBAGENT_PAINT[block.status](status)}`;
|
|
3983
|
+
const digest = firstLine(block.digest);
|
|
3984
|
+
const digestRoom = cols - width(named) - width(" \xB7 ");
|
|
3985
|
+
return digest && digestRoom >= 4 ? `${named}${style.grey(` \xB7 ${truncate(digest, digestRoom)}`)}` : named;
|
|
3986
|
+
}
|
|
3987
|
+
function subagentLines(block, cols, detailAll) {
|
|
3988
|
+
const header = subagentHeader(block, cols);
|
|
3989
|
+
if (!detailAll) return [header];
|
|
3990
|
+
const childCols = Math.max(1, cols - CHILD_INDENT.length);
|
|
3991
|
+
const children = block.children.flatMap((child) => blockLines(child, childCols, true).map((line) => CHILD_INDENT + line));
|
|
3992
|
+
const digest = sanitize(block.digest).trim();
|
|
3993
|
+
const handedBack = digest ? underHeader(wrap(digest, Math.max(1, cols - OUTPUT_INDENT.length)).map(style.grey)) : [];
|
|
3994
|
+
return [header, ...children, ...handedBack];
|
|
3995
|
+
}
|
|
3996
|
+
var APPROVAL_KIND = {
|
|
3997
|
+
shell_command: "Run a command",
|
|
3998
|
+
url_fetch: "Fetch a URL",
|
|
3999
|
+
external_path: "Read outside the workspace"
|
|
4000
|
+
};
|
|
4001
|
+
var DECIDED_BY = {
|
|
4002
|
+
"pre-approved": "pre-approved",
|
|
4003
|
+
remembered: "remembered",
|
|
4004
|
+
mode: "policy",
|
|
4005
|
+
"no-channel": "no approval channel"
|
|
4006
|
+
};
|
|
4007
|
+
function approvalLine(block, cols) {
|
|
4008
|
+
const policy = block.decidedBy && block.decidedBy !== "asked" ? DECIDED_BY[block.decidedBy] : void 0;
|
|
4009
|
+
const [mark, status, paint] = block.status === "pending" ? ["?", "Waiting for you", style.yellow] : block.status === "granted" ? ["\u2713", policy ? `Auto-approved (${policy})` : "Approved", style.green] : ["\u2298", policy ? `Auto-denied (${policy})` : "Denied", style.yellow];
|
|
4010
|
+
const line = `${mark} ${status} \xB7 ${APPROVAL_KIND[block.kind]}: ${firstLine(block.subject)}`;
|
|
4011
|
+
return `${paint(mark)}${truncate(line, cols).slice(mark.length)}`;
|
|
4012
|
+
}
|
|
4013
|
+
function planLine(block, cols) {
|
|
4014
|
+
const count = block.taskCount === void 0 ? "" : ` (${countOf(block.taskCount, "task")})`;
|
|
4015
|
+
const text = block.status === "building" ? "Building plan\u2026" : `Plan ${block.status}${count}`;
|
|
4016
|
+
return `${style.cyan("\u25C7")}${truncate(`\u25C7 ${text}`, cols).slice(1)}`;
|
|
4017
|
+
}
|
|
4018
|
+
function blockLines(block, cols, detailAll) {
|
|
4019
|
+
switch (block.type) {
|
|
4020
|
+
case "message":
|
|
4021
|
+
return messageLines(block, cols);
|
|
4022
|
+
case "thinking":
|
|
4023
|
+
return thinkingLines(block, cols, detailAll);
|
|
4024
|
+
case "tool":
|
|
4025
|
+
return toolLines(block, cols, detailAll);
|
|
4026
|
+
case "subagent":
|
|
4027
|
+
return subagentLines(block, cols, detailAll);
|
|
4028
|
+
case "approval":
|
|
4029
|
+
return [approvalLine(block, cols)];
|
|
4030
|
+
case "plan":
|
|
4031
|
+
return [planLine(block, cols)];
|
|
4032
|
+
// Pinned under the pane rather than drawn in it — see `tokenLine`.
|
|
4033
|
+
case "usage":
|
|
4034
|
+
return [];
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
var drawnBlocks = /* @__PURE__ */ new WeakMap();
|
|
4038
|
+
function drawn(block, cols, detailAll) {
|
|
4039
|
+
const cached = drawnBlocks.get(block);
|
|
4040
|
+
if (cached && cached.cols === cols && cached.detailAll === detailAll) return cached.lines;
|
|
4041
|
+
const lines = blockLines(block, cols, detailAll);
|
|
4042
|
+
drawnBlocks.set(block, { cols, detailAll, lines });
|
|
4043
|
+
return lines;
|
|
4044
|
+
}
|
|
4045
|
+
function conversationLines(blocks, cols, detailAll) {
|
|
4046
|
+
const lines = [];
|
|
4047
|
+
for (const block of blocks) {
|
|
4048
|
+
if (block.type === "usage") continue;
|
|
4049
|
+
lines.push(...drawn(block, cols, detailAll), "");
|
|
4050
|
+
}
|
|
4051
|
+
return lines;
|
|
4052
|
+
}
|
|
4053
|
+
function compactCount(n) {
|
|
4054
|
+
if (n < 1e3) return String(n);
|
|
4055
|
+
const [value, unit] = n < 999950 ? [n / 1e3, "k"] : [n / 1e6, "M"];
|
|
4056
|
+
return `${value.toFixed(1).replace(/\.0$/, "")}${unit}`;
|
|
4057
|
+
}
|
|
4058
|
+
function formatCost(currency, amount) {
|
|
4059
|
+
const [whole, frac = ""] = amount.toFixed(4).replace(/0+$/, "").split(".");
|
|
4060
|
+
const value = `${whole}.${frac.padEnd(2, "0")}`;
|
|
4061
|
+
return currency.toLowerCase() === "usd" ? `$${value}` : `${value} ${currency.toUpperCase()}`;
|
|
4062
|
+
}
|
|
4063
|
+
function tokenLine(blocks, cols) {
|
|
4064
|
+
const usage = blocks[blocks.length - 1];
|
|
4065
|
+
if (usage?.type !== "usage") return null;
|
|
4066
|
+
const { totals, contextFill } = usage;
|
|
4067
|
+
const parts = [
|
|
4068
|
+
...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : [],
|
|
4069
|
+
...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : [],
|
|
4070
|
+
...contextFill ? [`${Math.round(contextFill.usedTokens / contextFill.windowTokens * 100)}% ctx`] : [],
|
|
4071
|
+
...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount))
|
|
4072
|
+
];
|
|
4073
|
+
return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
|
|
4074
|
+
}
|
|
4075
|
+
|
|
3807
4076
|
// src/tui/geometry.ts
|
|
3808
4077
|
var PLAN_MIN_COLS = 32;
|
|
3809
4078
|
var PLAN_MAX_COLS = 64;
|
|
@@ -3835,7 +4104,7 @@ function chatEditorRoom(cols, active) {
|
|
|
3835
4104
|
}
|
|
3836
4105
|
|
|
3837
4106
|
// src/tui/state.ts
|
|
3838
|
-
var
|
|
4107
|
+
var import_core20 = require("@ordewell/core");
|
|
3839
4108
|
var SKILL_IDS = ["tdd", "verify"];
|
|
3840
4109
|
function noSkills() {
|
|
3841
4110
|
return { tdd: false, verify: false };
|
|
@@ -3849,6 +4118,7 @@ function visibleItems(picker2) {
|
|
|
3849
4118
|
}
|
|
3850
4119
|
var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
|
|
3851
4120
|
var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
|
|
4121
|
+
var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
|
|
3852
4122
|
function planRows(state) {
|
|
3853
4123
|
const rows = [];
|
|
3854
4124
|
for (const task of state.tasks) {
|
|
@@ -3875,10 +4145,11 @@ function findTask(tasks, id) {
|
|
|
3875
4145
|
function initialState(overrides = {}) {
|
|
3876
4146
|
return {
|
|
3877
4147
|
editor: emptyEditor(),
|
|
3878
|
-
|
|
4148
|
+
conversation: import_core20.EMPTY_CONVERSATION,
|
|
4149
|
+
turnGate: import_core20.NO_TURN,
|
|
4150
|
+
detailAll: false,
|
|
3879
4151
|
status: "idle",
|
|
3880
4152
|
busyLabel: "",
|
|
3881
|
-
thinkingLine: "",
|
|
3882
4153
|
sessionId: null,
|
|
3883
4154
|
goal: "",
|
|
3884
4155
|
tasks: [],
|
|
@@ -3900,7 +4171,7 @@ function initialState(overrides = {}) {
|
|
|
3900
4171
|
orchestratorModel: "",
|
|
3901
4172
|
plannerProvider: "",
|
|
3902
4173
|
plannerEffort: "",
|
|
3903
|
-
maxParallel:
|
|
4174
|
+
maxParallel: import_core20.DEFAULT_MAX_PARALLEL,
|
|
3904
4175
|
configuredProviders: [],
|
|
3905
4176
|
allowlist: {},
|
|
3906
4177
|
autonomous: true,
|
|
@@ -3910,6 +4181,10 @@ function initialState(overrides = {}) {
|
|
|
3910
4181
|
handoff: null,
|
|
3911
4182
|
overlay: null,
|
|
3912
4183
|
pendingApprovals: [],
|
|
4184
|
+
queuedPrompts: import_core20.EMPTY_HOLD,
|
|
4185
|
+
stopArmed: false,
|
|
4186
|
+
stopArmToken: 0,
|
|
4187
|
+
stopRequested: false,
|
|
3913
4188
|
toast: "",
|
|
3914
4189
|
rows: 24,
|
|
3915
4190
|
cols: 80,
|
|
@@ -3920,7 +4195,7 @@ function initialState(overrides = {}) {
|
|
|
3920
4195
|
}
|
|
3921
4196
|
|
|
3922
4197
|
// src/tui/layout.ts
|
|
3923
|
-
var
|
|
4198
|
+
var import_core21 = require("@ordewell/core");
|
|
3924
4199
|
var CHROME_ROWS = 4;
|
|
3925
4200
|
function chatInputWrap(state) {
|
|
3926
4201
|
const active = state.focus === "chat" && !state.overlay;
|
|
@@ -3931,8 +4206,8 @@ function footerHints(state) {
|
|
|
3931
4206
|
const selected = selectedPlanRow(state)?.task;
|
|
3932
4207
|
const markHint = selected?.status === "completed" ? "m undone" : "m done";
|
|
3933
4208
|
const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
|
|
3934
|
-
const planning = state
|
|
3935
|
-
const escHint = planning ? "esc stop planning"
|
|
4209
|
+
const planning = plannerInFlight(state);
|
|
4210
|
+
const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
|
|
3936
4211
|
if (state.focus === "plan") {
|
|
3937
4212
|
if (state.taskEditor) {
|
|
3938
4213
|
return ["type to edit prompt", "pgup/pgdn scroll", "alt-enter newline", "enter save", "esc cancel"];
|
|
@@ -3947,6 +4222,7 @@ function footerHints(state) {
|
|
|
3947
4222
|
"D deps",
|
|
3948
4223
|
"f start",
|
|
3949
4224
|
"E run plan",
|
|
4225
|
+
"S stop",
|
|
3950
4226
|
"c cancel",
|
|
3951
4227
|
markHint,
|
|
3952
4228
|
"s skip",
|
|
@@ -3958,15 +4234,27 @@ function footerHints(state) {
|
|
|
3958
4234
|
"tab chat"
|
|
3959
4235
|
];
|
|
3960
4236
|
}
|
|
4237
|
+
const detailHint = (0, import_core21.hasHiddenDetail)(state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
|
|
3961
4238
|
return [
|
|
3962
4239
|
"/help",
|
|
3963
4240
|
"tab plan",
|
|
3964
4241
|
"alt-enter newline",
|
|
3965
4242
|
"pgup/pgdn scroll",
|
|
4243
|
+
...detailHint,
|
|
3966
4244
|
...escHint ? [escHint] : [],
|
|
3967
4245
|
"ctrl-c quit"
|
|
3968
4246
|
];
|
|
3969
4247
|
}
|
|
4248
|
+
function queuedPromptRows(state, cols) {
|
|
4249
|
+
return state.queuedPrompts.flatMap((text) => queuedBubble(text, cols));
|
|
4250
|
+
}
|
|
4251
|
+
function queuedBubble(text, cols) {
|
|
4252
|
+
const room = Math.max(1, cols - width("\u25C7 \xB7 queued \xB7 esc to unsend"));
|
|
4253
|
+
return wrap(text, room).map((line, i) => i === 0 ? truncate(`${style.grey(`\u25C7 ${line}`)} \xB7 queued \xB7 esc to unsend`, cols) : truncate(style.grey(` ${line}`), cols));
|
|
4254
|
+
}
|
|
4255
|
+
function stopHint(state) {
|
|
4256
|
+
return plannerInFlight(state) && state.stopArmed ? "Press Esc again to stop" : "";
|
|
4257
|
+
}
|
|
3970
4258
|
function packHints(hints, cols) {
|
|
3971
4259
|
const lines = [];
|
|
3972
4260
|
let current = "";
|
|
@@ -3987,78 +4275,33 @@ function bodyRows(state) {
|
|
|
3987
4275
|
const footer = packHints(footerHints(state), state.cols).length;
|
|
3988
4276
|
return Math.max(0, state.rows - CHROME_ROWS - input - footer + 2);
|
|
3989
4277
|
}
|
|
3990
|
-
var ROLE_PREFIX = {
|
|
3991
|
-
user: (t) => `${style.cyan("\u276F")} ${t}`,
|
|
3992
|
-
assistant: (t) => `${style.magenta("\u25C6")} ${t}`,
|
|
3993
|
-
system: (t) => style.grey(`\xB7 ${t}`),
|
|
3994
|
-
error: (t) => `${style.red("\u2717")} ${style.red(t)}`,
|
|
3995
|
-
// Research rows go through `researchRow`, which paints the outcome mark
|
|
3996
|
-
// separately; this entry only exists to keep the role map total.
|
|
3997
|
-
research: (t) => style.grey(` \u21B3 ${t}`)
|
|
3998
|
-
};
|
|
3999
|
-
var OUTCOME_MARK2 = {
|
|
4000
|
-
success: "\u2713",
|
|
4001
|
-
failure: "\u2717",
|
|
4002
|
-
refused: "\u2298",
|
|
4003
|
-
denied: "\u2298",
|
|
4004
|
-
not_executed: "\u2013"
|
|
4005
|
-
};
|
|
4006
|
-
var OUTCOME_PAINT = {
|
|
4007
|
-
success: style.grey,
|
|
4008
|
-
failure: style.red,
|
|
4009
|
-
refused: style.yellow,
|
|
4010
|
-
denied: style.yellow,
|
|
4011
|
-
not_executed: style.grey
|
|
4012
|
-
};
|
|
4013
|
-
var RESEARCH_RESULT_CHARS = 90;
|
|
4014
|
-
function researchLine(message) {
|
|
4015
|
-
const meta = message.research;
|
|
4016
|
-
if (!meta?.outcome) return `\u22EF ${message.content}`;
|
|
4017
|
-
const result = (meta.result ?? "").replace(/\s+/g, " ").trim();
|
|
4018
|
-
const preview = result.length > RESEARCH_RESULT_CHARS ? `${result.slice(0, RESEARCH_RESULT_CHARS)}\u2026` : result;
|
|
4019
|
-
const mark = OUTCOME_MARK2[meta.outcome];
|
|
4020
|
-
return preview ? `${mark} ${message.content} \u2192 ${preview}` : `${mark} ${message.content}`;
|
|
4021
|
-
}
|
|
4022
|
-
var RESEARCH_INDENT = " \u21B3 ";
|
|
4023
|
-
function researchRow(message, cols) {
|
|
4024
|
-
const text = truncate(researchLine(message), Math.max(1, cols - width(RESEARCH_INDENT)));
|
|
4025
|
-
const paint = OUTCOME_PAINT[message.research?.outcome ?? "success"];
|
|
4026
|
-
const space = text.indexOf(" ");
|
|
4027
|
-
if (space < 0) return style.grey(`${RESEARCH_INDENT}${text}`);
|
|
4028
|
-
return `${style.grey(" \u21B3")} ${paint(text.slice(0, space))} ${style.grey(text.slice(space + 1))}`;
|
|
4029
|
-
}
|
|
4030
4278
|
var chatBodyMemo = null;
|
|
4031
|
-
function chatBodyLines(
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
for (const message of messages) {
|
|
4037
|
-
if (message.role === "research") {
|
|
4038
|
-
lines.push(researchRow(message, cols));
|
|
4039
|
-
continue;
|
|
4040
|
-
}
|
|
4041
|
-
const wrapped = message.role === "assistant" ? renderMarkdown(message.content, Math.max(1, cols - 2)) : wrap(message.content, Math.max(1, cols - 2));
|
|
4042
|
-
lines.push(...wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[message.role](line) : ` ${line}`));
|
|
4043
|
-
lines.push("");
|
|
4044
|
-
}
|
|
4045
|
-
chatBodyMemo = { messages, cols, lines };
|
|
4279
|
+
function chatBodyLines(blocks, cols, detailAll) {
|
|
4280
|
+
const memo = chatBodyMemo;
|
|
4281
|
+
if (memo && memo.blocks === blocks && memo.cols === cols && memo.detailAll === detailAll) return memo.lines;
|
|
4282
|
+
const lines = conversationLines(blocks, cols, detailAll);
|
|
4283
|
+
chatBodyMemo = { blocks, cols, detailAll, lines };
|
|
4046
4284
|
return lines;
|
|
4047
4285
|
}
|
|
4048
4286
|
function chatLayout(state, rows, cols) {
|
|
4049
4287
|
const welcome = state.tasks.length === 0;
|
|
4050
|
-
const body = chatBodyLines(state.
|
|
4051
|
-
const
|
|
4052
|
-
|
|
4288
|
+
const body = chatBodyLines(state.conversation.blocks, cols, state.detailAll);
|
|
4289
|
+
const transcript = !welcome ? body : body.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
|
|
4290
|
+
const bubbles = queuedPromptRows(state, cols);
|
|
4291
|
+
const lines = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
|
|
4292
|
+
const usage = rows > 1 ? tokenLine(state.conversation.blocks, cols) : null;
|
|
4293
|
+
const footer = usage ? [usage] : [];
|
|
4294
|
+
const room = rows - footer.length;
|
|
4295
|
+
return { lines, footer, anchor: lines.length > room ? "bottom" : "top", maxScroll: Math.max(0, lines.length - room) };
|
|
4053
4296
|
}
|
|
4054
4297
|
function chatScrollMax(state) {
|
|
4055
4298
|
return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
|
|
4056
4299
|
}
|
|
4057
4300
|
function plannerSetup(state) {
|
|
4058
4301
|
const provider = state.plannerProvider;
|
|
4059
|
-
const registration =
|
|
4302
|
+
const registration = import_core21.ALL_PROVIDERS[provider];
|
|
4060
4303
|
if (!registration) return { label: "not set", ready: false };
|
|
4061
|
-
const runner = (0,
|
|
4304
|
+
const runner = (0, import_core21.runnerForProvider)(provider);
|
|
4062
4305
|
const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
|
|
4063
4306
|
return { label: registration.label, ready };
|
|
4064
4307
|
}
|
|
@@ -4241,7 +4484,7 @@ function taskLines(state, row, index, cols) {
|
|
|
4241
4484
|
const icon = paint(rawIcon);
|
|
4242
4485
|
const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
|
|
4243
4486
|
const caret = selected ? style.cyan("\u276F") : " ";
|
|
4244
|
-
const orderLabel = parent ? (0,
|
|
4487
|
+
const orderLabel = parent ? (0, import_core21.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
|
|
4245
4488
|
const indent = parent ? " " : "";
|
|
4246
4489
|
const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
|
|
4247
4490
|
const titleRoom = Math.max(1, cols - width(head));
|
|
@@ -4268,16 +4511,16 @@ function taskLines(state, row, index, cols) {
|
|
|
4268
4511
|
if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
|
|
4269
4512
|
if (task.isolation?.state === "conflict") {
|
|
4270
4513
|
const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
|
|
4271
|
-
const files = task.isolation.conflictFiles?.length ? ` (${(0,
|
|
4514
|
+
const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core21.capConflictFiles)(task.isolation.conflictFiles)})` : "";
|
|
4272
4515
|
lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
|
|
4273
4516
|
}
|
|
4274
4517
|
if (task.isolation?.state === "repairing") {
|
|
4275
|
-
const files = task.isolation.conflictFiles?.length ? ` in ${(0,
|
|
4518
|
+
const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core21.capConflictFiles)(task.isolation.conflictFiles)}` : "";
|
|
4276
4519
|
const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
|
|
4277
4520
|
lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
|
|
4278
4521
|
}
|
|
4279
4522
|
if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
|
|
4280
|
-
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0,
|
|
4523
|
+
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core21.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
|
|
4281
4524
|
}
|
|
4282
4525
|
let editorLine;
|
|
4283
4526
|
if (expanded) {
|
|
@@ -4351,7 +4594,7 @@ function helpLayout(rows, cols) {
|
|
|
4351
4594
|
body.push("");
|
|
4352
4595
|
}
|
|
4353
4596
|
body.push(
|
|
4354
|
-
style.grey("tab switches panes \xB7
|
|
4597
|
+
style.grey("tab switches panes \xB7 pgup/pgdn scroll \xB7 ctrl-o toggles full detail \xB7 esc takes back a queued prompt, otherwise esc twice stops \xB7 ctrl-l clears \xB7 ctrl-c quits")
|
|
4355
4598
|
);
|
|
4356
4599
|
const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
|
|
4357
4600
|
const room = Math.max(1, rows - 2);
|
|
@@ -4362,7 +4605,7 @@ function helpScrollMax(state) {
|
|
|
4362
4605
|
}
|
|
4363
4606
|
|
|
4364
4607
|
// src/tui/render.ts
|
|
4365
|
-
var
|
|
4608
|
+
var import_core22 = require("@ordewell/core");
|
|
4366
4609
|
function render(state) {
|
|
4367
4610
|
const { rows, cols } = state;
|
|
4368
4611
|
const rowsForBody = bodyRows(state);
|
|
@@ -4465,20 +4708,16 @@ function scrolledBackMark(state) {
|
|
|
4465
4708
|
}
|
|
4466
4709
|
function renderStatus(state, cols) {
|
|
4467
4710
|
const mark = scrolledBackMark(state);
|
|
4711
|
+
const armed = stopHint(state);
|
|
4468
4712
|
if (state.status === "idle") {
|
|
4469
4713
|
const notes = [mark, state.toast].filter(Boolean).join(" \xB7 ");
|
|
4470
4714
|
return notes ? truncate(style.grey(notes), cols) : "";
|
|
4471
4715
|
}
|
|
4716
|
+
if (armed) return truncate(style.red(armed), cols);
|
|
4472
4717
|
const scrolled = mark ? style.grey(`${mark} \xB7 `) : "";
|
|
4473
4718
|
const label = state.busyLabel ? ` ${state.busyLabel}` : "";
|
|
4474
4719
|
const verb = state.status === "executing" ? "Executing" : state.status === "researching" ? "Researching" : "Planning";
|
|
4475
|
-
|
|
4476
|
-
const thinking = state.thinkingLine.replace(/\s+/g, " ").trim();
|
|
4477
|
-
if (!thinking) return truncate(head, cols);
|
|
4478
|
-
const room = cols - width(head) - 3;
|
|
4479
|
-
if (room < 12) return truncate(head, cols);
|
|
4480
|
-
const tail = thinking.length > room ? `\u2026${thinking.slice(-(room - 1)).trimStart()}` : thinking;
|
|
4481
|
-
return truncate(`${head}${style.grey(` \xB7 ${tail}`)}`, cols);
|
|
4720
|
+
return truncate(scrolled + style.yellow(`\u25CF ${verb}\u2026${label}`), cols);
|
|
4482
4721
|
}
|
|
4483
4722
|
function highlightSpans(text) {
|
|
4484
4723
|
const spans = [];
|
|
@@ -4560,9 +4799,9 @@ function renderBody(state, rows, cols) {
|
|
|
4560
4799
|
);
|
|
4561
4800
|
}
|
|
4562
4801
|
function renderChat(state, rows, cols) {
|
|
4563
|
-
const { lines, anchor, maxScroll } = chatLayout(state, rows, cols);
|
|
4802
|
+
const { lines, footer, anchor, maxScroll } = chatLayout(state, rows, cols);
|
|
4564
4803
|
const back = Math.min(state.scroll, maxScroll);
|
|
4565
|
-
return fit(lines.slice(0, lines.length - back), rows, anchor);
|
|
4804
|
+
return [...fit(lines.slice(0, lines.length - back), rows - footer.length, anchor), ...footer];
|
|
4566
4805
|
}
|
|
4567
4806
|
function renderPlan(state, rows, cols) {
|
|
4568
4807
|
const layout = planLayout(state, rows, cols);
|
|
@@ -4595,7 +4834,9 @@ function renderOverlay(state, rows, cols) {
|
|
|
4595
4834
|
// ESC is not "deny" while a turn is in flight — it kills the turn and
|
|
4596
4835
|
// takes this prompt with it. Offering it as the deny key there would
|
|
4597
4836
|
// teach the wrong thing about the most destructive key on the sheet.
|
|
4598
|
-
|
|
4837
|
+
// With a prompt queued the first ESC unsends that instead, so the hint
|
|
4838
|
+
// names the take-back rather than the twice-to-stop.
|
|
4839
|
+
style.grey(plannerInFlight(state) ? state.queuedPrompts.length > 0 ? "y or enter allows \xB7 n denies \xB7 esc takes back the queued prompt" : "y or enter allows \xB7 n denies \xB7 esc twice stops planning" : "y or enter allows \xB7 n or esc denies")
|
|
4599
4840
|
],
|
|
4600
4841
|
rows,
|
|
4601
4842
|
cols
|
|
@@ -4626,7 +4867,7 @@ function renderHandoff(state, overlay, rows, cols) {
|
|
|
4626
4867
|
return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
|
|
4627
4868
|
}
|
|
4628
4869
|
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
|
|
4629
|
-
const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0,
|
|
4870
|
+
const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core22.capConflictFiles)(task.repairedFiles)})`) : "";
|
|
4630
4871
|
return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
|
|
4631
4872
|
});
|
|
4632
4873
|
const actions = handoffActions(handoff).map((action, index) => {
|
|
@@ -4716,412 +4957,111 @@ function frame(title, body, rows, cols) {
|
|
|
4716
4957
|
}
|
|
4717
4958
|
|
|
4718
4959
|
// src/tui/reducer.ts
|
|
4719
|
-
var
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
var DEFAULT_EFFORT = "__runner_default__";
|
|
4960
|
+
var import_core26 = require("@ordewell/core");
|
|
4961
|
+
|
|
4962
|
+
// src/tui/reducers/shared.ts
|
|
4723
4963
|
function step(state, effects = []) {
|
|
4724
4964
|
return { state, effects };
|
|
4725
4965
|
}
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
timestamp: entry.timestamp
|
|
4731
|
-
}));
|
|
4966
|
+
var fail3 = (state, message) => step(say(state, "error", message));
|
|
4967
|
+
var stale = (state, sessionId) => sessionId !== void 0 && sessionId !== state.sessionId;
|
|
4968
|
+
function clamp(value, max) {
|
|
4969
|
+
return Math.max(0, Math.min(max, value));
|
|
4732
4970
|
}
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
const last = findLastIndex(messages, (m) => m.role === "assistant" || m.role === "user");
|
|
4736
|
-
const spoken = last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
|
|
4737
|
-
const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
|
|
4738
|
-
return spoken || redrawn;
|
|
4971
|
+
function clampSelection(index, rows) {
|
|
4972
|
+
return Math.max(0, Math.min(index, rows - 1));
|
|
4739
4973
|
}
|
|
4740
|
-
function
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
const [first] = running;
|
|
4744
|
-
const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
|
|
4745
|
-
return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
|
|
4974
|
+
function withSession(state, run) {
|
|
4975
|
+
if (!state.sessionId) {
|
|
4976
|
+
return fail3(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
|
|
4746
4977
|
}
|
|
4747
|
-
|
|
4748
|
-
if (waiting === 0) return "";
|
|
4749
|
-
return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
|
|
4978
|
+
return run(state.sessionId);
|
|
4750
4979
|
}
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
};
|
|
4764
|
-
return
|
|
4980
|
+
|
|
4981
|
+
// src/tui/reducers/approvals.ts
|
|
4982
|
+
function enqueueApproval(state, request) {
|
|
4983
|
+
const open = state.overlay?.kind === "approval" ? state.overlay.request : null;
|
|
4984
|
+
if (open?.id === request.id) return state;
|
|
4985
|
+
if (state.pendingApprovals.some((p) => p.id === request.id)) return state;
|
|
4986
|
+
if (open) return { ...state, pendingApprovals: [...state.pendingApprovals, request] };
|
|
4987
|
+
return { ...state, overlay: { kind: "approval", request } };
|
|
4988
|
+
}
|
|
4989
|
+
function dropApproval(state, approvalId) {
|
|
4990
|
+
const pending = state.pendingApprovals.filter((p) => p.id !== approvalId);
|
|
4991
|
+
const open = state.overlay?.kind === "approval" ? state.overlay.request : null;
|
|
4992
|
+
if (open?.id !== approvalId) return { ...state, pendingApprovals: pending };
|
|
4993
|
+
return showNextApproval({ ...state, pendingApprovals: pending, overlay: null });
|
|
4994
|
+
}
|
|
4995
|
+
function showNextApproval(state) {
|
|
4996
|
+
const [next, ...rest] = state.pendingApprovals;
|
|
4997
|
+
if (!next) return { ...state, overlay: null };
|
|
4998
|
+
return { ...state, overlay: { kind: "approval", request: next }, pendingApprovals: rest };
|
|
4999
|
+
}
|
|
5000
|
+
function handleApprovalKey(state, overlay, key) {
|
|
5001
|
+
const grant = key.name === "enter" || key.char === "y" || key.char === "Y";
|
|
5002
|
+
const deny = key.name === "escape" || key.char === "n" || key.char === "N";
|
|
5003
|
+
if (!grant && !deny) return step(state);
|
|
5004
|
+
const { request } = overlay;
|
|
5005
|
+
return step(showNextApproval({ ...state, overlay: null }), state.sessionId ? [{ type: "respondApproval", sessionId: state.sessionId, approvalId: request.id, granted: grant }] : []);
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
// src/tui/reducers/commands.ts
|
|
5009
|
+
var import_core25 = require("@ordewell/core");
|
|
5010
|
+
|
|
5011
|
+
// src/tui/reducers/pickers.ts
|
|
5012
|
+
var import_core23 = require("@ordewell/core");
|
|
5013
|
+
var import_plan_utils2 = require("@ordewell/core/plan-utils");
|
|
5014
|
+
var DEFAULT_EFFORT = "__runner_default__";
|
|
5015
|
+
function picker(title, items, action, extra = {}) {
|
|
5016
|
+
return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
|
|
5017
|
+
}
|
|
5018
|
+
function refillPicker(state, kinds) {
|
|
5019
|
+
const overlay = state.overlay;
|
|
5020
|
+
if (overlay?.kind !== "picker" || !kinds.includes(overlay.picker.action.kind)) return state;
|
|
5021
|
+
const items = pickerItemsFor(state, overlay.picker.action);
|
|
5022
|
+
const hint = overlay.picker.action.kind === "set-model" ? providerErrorHint(state) : overlay.picker.hint;
|
|
5023
|
+
return { ...state, overlay: { kind: "picker", picker: { ...overlay.picker, items, hint, index: 0 } } };
|
|
5024
|
+
}
|
|
5025
|
+
function modelDetail(m) {
|
|
5026
|
+
return [m.provider, m.pricing].filter(Boolean).join(" \xB7 ") || void 0;
|
|
5027
|
+
}
|
|
5028
|
+
function providerErrorHint(state) {
|
|
5029
|
+
const failed = Object.keys(state.providerErrors ?? {});
|
|
5030
|
+
if (failed.length === 0) return void 0;
|
|
5031
|
+
const names = failed.map((p) => import_core23.ALL_PROVIDERS[p]?.label ?? p);
|
|
5032
|
+
return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
|
|
5033
|
+
}
|
|
5034
|
+
function plannerModelItems(state) {
|
|
5035
|
+
const runner = (0, import_core23.runnerForProvider)(state.plannerProvider);
|
|
5036
|
+
if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
|
|
5037
|
+
const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
|
|
5038
|
+
id: m.id,
|
|
5039
|
+
label: m.label,
|
|
5040
|
+
detail: [
|
|
5041
|
+
m.provider,
|
|
5042
|
+
m.variants?.length ? `${m.variants.length} effort level${m.variants.length === 1 ? "" : "s"}` : "runner default effort"
|
|
5043
|
+
].filter(Boolean).join(" \xB7 "),
|
|
5044
|
+
selected: m.id === state.orchestratorModel
|
|
5045
|
+
}));
|
|
5046
|
+
return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
|
|
4765
5047
|
}
|
|
4766
|
-
function
|
|
4767
|
-
|
|
4768
|
-
|
|
5048
|
+
function plannerEffortItems(state) {
|
|
5049
|
+
const runner = (0, import_core23.runnerForProvider)(state.plannerProvider);
|
|
5050
|
+
if (!runner) {
|
|
5051
|
+
return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
|
|
5052
|
+
}
|
|
5053
|
+
const model = state.models.find((m) => m.id === state.orchestratorModel && m.runners?.includes(runner));
|
|
5054
|
+
if (!model) {
|
|
5055
|
+
return [{ id: "", label: "No planner model selected", detail: "pick one with /model", disabled: true }];
|
|
5056
|
+
}
|
|
5057
|
+
const variants = model.variants ?? [];
|
|
5058
|
+
if (variants.length === 0) {
|
|
5059
|
+
return [{ id: "", label: `${model.label} exposes no effort levels`, detail: "it always runs at the agent's default", disabled: true }];
|
|
4769
5060
|
}
|
|
4770
|
-
return
|
|
4771
|
-
}
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
var stale = (state, sessionId) => sessionId !== void 0 && sessionId !== state.sessionId;
|
|
4775
|
-
function reduce(state, action) {
|
|
4776
|
-
switch (action.type) {
|
|
4777
|
-
case "key":
|
|
4778
|
-
return handleKey2(state, action.key);
|
|
4779
|
-
// Isolation belongs to one session's plan, and task ids repeat across
|
|
4780
|
-
// sessions, so nothing of the last one's may carry over into this.
|
|
4781
|
-
case "sessionStarted":
|
|
4782
|
-
return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
|
|
4783
|
-
// The daemon registers a session only once planning succeeds; when it does
|
|
4784
|
-
// not, holding on to the id would send every next message to a session the
|
|
4785
|
-
// server never had.
|
|
4786
|
-
case "sessionCleared":
|
|
4787
|
-
return step({
|
|
4788
|
-
...clearIsolation(state),
|
|
4789
|
-
sessionId: null,
|
|
4790
|
-
goal: "",
|
|
4791
|
-
pendingApprovals: [],
|
|
4792
|
-
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
4793
|
-
});
|
|
4794
|
-
case "approvalRequested": {
|
|
4795
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4796
|
-
return step(enqueueApproval(state, action.request));
|
|
4797
|
-
}
|
|
4798
|
-
case "approvalSettled": {
|
|
4799
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4800
|
-
return step(dropApproval(state, action.approvalId));
|
|
4801
|
-
}
|
|
4802
|
-
case "chatRestored":
|
|
4803
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4804
|
-
return step({ ...state, messages: restoredMessages(action.history), scroll: 0 });
|
|
4805
|
-
case "planUpdated": {
|
|
4806
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4807
|
-
const isolation = isolationOfPlan(action.plan);
|
|
4808
|
-
const tasks = isolationForPlan(normalizeTasks(action.plan), isolation, state.tasks);
|
|
4809
|
-
const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
|
|
4810
|
-
const next = {
|
|
4811
|
-
...state,
|
|
4812
|
-
tasks,
|
|
4813
|
-
handoff: isolation ? isolation.handoff : state.handoff,
|
|
4814
|
-
status: state.status === "planning" || state.status === "researching" ? "idle" : state.status,
|
|
4815
|
-
busyLabel: "",
|
|
4816
|
-
thinkingLine: "",
|
|
4817
|
-
expandedTaskId,
|
|
4818
|
-
taskEditor: expandedTaskId !== null ? state.taskEditor : null
|
|
4819
|
-
};
|
|
4820
|
-
const selectedTask = clampSelection(state.selectedTask, planRows(next).length);
|
|
4821
|
-
return step(settlePlan({ ...next, selectedTask, focus: tasks.length === 0 ? "chat" : state.focus }));
|
|
4822
|
-
}
|
|
4823
|
-
case "plannerMessage": {
|
|
4824
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4825
|
-
const settled = { ...state, status: "idle", busyLabel: "", thinkingLine: "" };
|
|
4826
|
-
const content = sanitize(action.content);
|
|
4827
|
-
const last = state.messages.at(-1);
|
|
4828
|
-
if (last?.role === "assistant" && last.streaming) {
|
|
4829
|
-
const messages = [...state.messages];
|
|
4830
|
-
messages[messages.length - 1] = { ...last, content, streaming: void 0 };
|
|
4831
|
-
return step({ ...settled, messages });
|
|
4832
|
-
}
|
|
4833
|
-
if (alreadySpoken(state.messages, content)) return step(settled);
|
|
4834
|
-
return step(say(settled, "assistant", content));
|
|
4835
|
-
}
|
|
4836
|
-
case "plannerToken": {
|
|
4837
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4838
|
-
const text = sanitize(action.text);
|
|
4839
|
-
const last = state.messages.at(-1);
|
|
4840
|
-
if (last?.role === "assistant" && last.streaming) {
|
|
4841
|
-
const messages = [...state.messages];
|
|
4842
|
-
messages[messages.length - 1] = { ...last, content: last.content + text };
|
|
4843
|
-
return step({ ...state, messages });
|
|
4844
|
-
}
|
|
4845
|
-
return step(say(state, "assistant", text, void 0, { streaming: true }));
|
|
4846
|
-
}
|
|
4847
|
-
case "researchStep": {
|
|
4848
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4849
|
-
const summary = sanitize(action.summary);
|
|
4850
|
-
const spoken = say(state, "research", summary, { toolCallId: action.toolCallId });
|
|
4851
|
-
const status = state.status === "planning" ? "researching" : state.status;
|
|
4852
|
-
return step({ ...spoken, status, busyLabel: researchLabel(spoken.messages, summary) });
|
|
4853
|
-
}
|
|
4854
|
-
// A settled transcript line, not a research step: nothing ever settles a
|
|
4855
|
-
// task's start, so as a step it stayed "⋯" forever and was counted into
|
|
4856
|
-
// the planner's next "(+N more)".
|
|
4857
|
-
case "taskStarted": {
|
|
4858
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4859
|
-
const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
|
|
4860
|
-
const who = action.runner ? ` \xB7 ${action.runner}` : "";
|
|
4861
|
-
const spoken = say(state, "system", sanitize(`Started "${action.title}"${who}`));
|
|
4862
|
-
return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
|
|
4863
|
-
}
|
|
4864
|
-
case "researchStepDone": {
|
|
4865
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4866
|
-
const messages = settleResearchStep(state.messages, action);
|
|
4867
|
-
const pending = messages.filter(isPendingResearch);
|
|
4868
|
-
const status = pending.length === 0 && state.status === "researching" ? "planning" : state.status;
|
|
4869
|
-
return step({
|
|
4870
|
-
...state,
|
|
4871
|
-
messages,
|
|
4872
|
-
status,
|
|
4873
|
-
busyLabel: pending.length > 0 ? researchLabel(messages, pending[pending.length - 1].content) : ""
|
|
4874
|
-
});
|
|
4875
|
-
}
|
|
4876
|
-
case "plannerThinking": {
|
|
4877
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4878
|
-
return step({ ...state, thinkingLine: (state.thinkingLine + sanitize(action.text)).slice(-THINKING_TAIL) });
|
|
4879
|
-
}
|
|
4880
|
-
case "taskStatus": {
|
|
4881
|
-
if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
|
|
4882
|
-
return step({
|
|
4883
|
-
...state,
|
|
4884
|
-
status: "executing",
|
|
4885
|
-
tasks: state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status } : t)
|
|
4886
|
-
});
|
|
4887
|
-
}
|
|
4888
|
-
case "tasksStatus": {
|
|
4889
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4890
|
-
const updates = action.updates;
|
|
4891
|
-
let changed = false;
|
|
4892
|
-
const tasks = state.tasks.map((t) => {
|
|
4893
|
-
const update = updates[t.id];
|
|
4894
|
-
if (update === void 0) return t;
|
|
4895
|
-
const idleSince = update.idleSince ?? null;
|
|
4896
|
-
const isolation = update.isolation ?? t.isolation;
|
|
4897
|
-
if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
|
|
4898
|
-
changed = true;
|
|
4899
|
-
return { ...t, status: update.status, idleSince, isolation };
|
|
4900
|
-
}
|
|
4901
|
-
return t;
|
|
4902
|
-
});
|
|
4903
|
-
if (!changed) return step(state);
|
|
4904
|
-
return step({ ...state, status: "executing", tasks, busyLabel: runLabel(tasks) });
|
|
4905
|
-
}
|
|
4906
|
-
case "queueReady": {
|
|
4907
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4908
|
-
const sessionId = action.sessionId ?? state.sessionId;
|
|
4909
|
-
if (sessionId === null) return step(state);
|
|
4910
|
-
return step(state, [{ type: "processQueued", sessionId }]);
|
|
4911
|
-
}
|
|
4912
|
-
case "isolationBlocked": {
|
|
4913
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4914
|
-
return step(blockedPicker(state, action.message, action.repos));
|
|
4915
|
-
}
|
|
4916
|
-
case "isolationHandoff": {
|
|
4917
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4918
|
-
return step(handoffArrived(state, action.handoff));
|
|
4919
|
-
}
|
|
4920
|
-
case "handoffDiff": {
|
|
4921
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4922
|
-
return step(showDiff(state, action.diff));
|
|
4923
|
-
}
|
|
4924
|
-
case "runCleared": {
|
|
4925
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4926
|
-
const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
|
|
4927
|
-
return step({ ...clearIsolation(state), overlay });
|
|
4928
|
-
}
|
|
4929
|
-
case "executionComplete": {
|
|
4930
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4931
|
-
const { completed, total, failed } = action.summary ?? {
|
|
4932
|
-
completed: state.tasks.filter((t) => t.status === "completed").length,
|
|
4933
|
-
total: state.tasks.length,
|
|
4934
|
-
failed: state.tasks.filter((t) => t.status === "failed").length
|
|
4935
|
-
};
|
|
4936
|
-
const failures = failed > 0 ? ` \xB7 ${failed} failed` : "";
|
|
4937
|
-
const verb = action.stopped ? "stopped" : "finished";
|
|
4938
|
-
return step({
|
|
4939
|
-
...say(state, "system", `Execution ${verb} \u2014 ${completed}/${total} tasks complete${failures}.`),
|
|
4940
|
-
status: "idle",
|
|
4941
|
-
busyLabel: "",
|
|
4942
|
-
thinkingLine: ""
|
|
4943
|
-
});
|
|
4944
|
-
}
|
|
4945
|
-
case "settingsLoaded":
|
|
4946
|
-
return step(applySettings(state, action.settings));
|
|
4947
|
-
case "modelsLoaded":
|
|
4948
|
-
return step(
|
|
4949
|
-
refillPicker(
|
|
4950
|
-
{
|
|
4951
|
-
...state,
|
|
4952
|
-
models: action.models,
|
|
4953
|
-
orchestratorModels: action.orchestratorModels ?? state.orchestratorModels,
|
|
4954
|
-
configuredProviders: action.providers ?? state.configuredProviders,
|
|
4955
|
-
providerErrors: action.providerErrors ?? state.providerErrors,
|
|
4956
|
-
modesByRunner: action.modesByRunner ?? state.modesByRunner
|
|
4957
|
-
},
|
|
4958
|
-
["set-model", "set-planner", "set-task-model", "set-task-effort", "set-task-mode"]
|
|
4959
|
-
)
|
|
4960
|
-
);
|
|
4961
|
-
case "sessionsLoaded":
|
|
4962
|
-
return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
|
|
4963
|
-
case "rewindTargetsLoaded":
|
|
4964
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4965
|
-
if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
|
|
4966
|
-
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
4967
|
-
// A fork holds no run, whatever the session it came from was doing — the
|
|
4968
|
-
// original keeps its run, and its events are stale from here on.
|
|
4969
|
-
case "sessionForked":
|
|
4970
|
-
return step({
|
|
4971
|
-
...clearIsolation(state),
|
|
4972
|
-
sessionId: action.sessionId,
|
|
4973
|
-
goal: action.goal,
|
|
4974
|
-
status: "idle",
|
|
4975
|
-
busyLabel: "",
|
|
4976
|
-
thinkingLine: "",
|
|
4977
|
-
planApproved: false,
|
|
4978
|
-
pendingApprovals: [],
|
|
4979
|
-
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
4980
|
-
});
|
|
4981
|
-
// The rewound message comes back as a draft to edit and resend, so it lands
|
|
4982
|
-
// as if just typed: history is not being browsed, and nothing is parked.
|
|
4983
|
-
case "inputPrefilled":
|
|
4984
|
-
if (stale(state, action.sessionId)) return step(state);
|
|
4985
|
-
return step({
|
|
4986
|
-
...state,
|
|
4987
|
-
focus: "chat",
|
|
4988
|
-
editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
4989
|
-
});
|
|
4990
|
-
case "runnersLoaded":
|
|
4991
|
-
return step(refillPicker({
|
|
4992
|
-
...state,
|
|
4993
|
-
runners: action.runners,
|
|
4994
|
-
orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
|
|
4995
|
-
}, ["set-planner", "set-task-runner"]));
|
|
4996
|
-
case "failed":
|
|
4997
|
-
return step({ ...say(state, "error", action.message), status: "idle", busyLabel: "", thinkingLine: "" });
|
|
4998
|
-
case "workspaceNeedsInit":
|
|
4999
|
-
return step({
|
|
5000
|
-
...state,
|
|
5001
|
-
status: "idle",
|
|
5002
|
-
busyLabel: "",
|
|
5003
|
-
thinkingLine: "",
|
|
5004
|
-
overlay: {
|
|
5005
|
-
kind: "confirm",
|
|
5006
|
-
title: "Initialize new workspace?",
|
|
5007
|
-
message: `"${action.workspace}" isn't a recognized project yet (no .git or manifest found). Start a new ordewell workspace here?`,
|
|
5008
|
-
action: { kind: "init-workspace", goal: action.goal, workspace: action.workspace }
|
|
5009
|
-
}
|
|
5010
|
-
});
|
|
5011
|
-
case "notice":
|
|
5012
|
-
return step(say(state, "system", action.message));
|
|
5013
|
-
case "resize": {
|
|
5014
|
-
const resized = { ...state, rows: action.rows, cols: action.cols, selection: null };
|
|
5015
|
-
return step(settlePlan({ ...resized, scroll: clamp(resized.scroll, chatScrollMax(resized)) }));
|
|
5016
|
-
}
|
|
5017
|
-
// Whether there is anything to animate is the app loop's call (it owns the
|
|
5018
|
-
// timer); a tick that arrives simply advances the frame.
|
|
5019
|
-
case "spinnerTick":
|
|
5020
|
-
return step({ ...state, spinnerFrame: (state.spinnerFrame + 1) % 10 });
|
|
5021
|
-
}
|
|
5022
|
-
}
|
|
5023
|
-
var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
|
|
5024
|
-
var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
|
|
5025
|
-
function normalizeTasks(plan) {
|
|
5026
|
-
const source = plan;
|
|
5027
|
-
const byId = /* @__PURE__ */ new Map();
|
|
5028
|
-
for (const list of [source?.pendingTasks, source?.executionLog]) {
|
|
5029
|
-
for (const task of list ?? []) {
|
|
5030
|
-
byId.set(String(task.id), task);
|
|
5031
|
-
}
|
|
5032
|
-
}
|
|
5033
|
-
const raw = byId.size > 0 ? [...byId.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) : source?.tasks ?? [];
|
|
5034
|
-
return raw.map((t, i) => toTaskView2(t, i));
|
|
5035
|
-
}
|
|
5036
|
-
function toTaskView2(t, index) {
|
|
5037
|
-
return {
|
|
5038
|
-
id: String(t.id ?? `task-${index + 1}`),
|
|
5039
|
-
order: typeof t.order === "number" ? t.order : index + 1,
|
|
5040
|
-
title: planLabel(t.title, "Untitled task"),
|
|
5041
|
-
description: planText(t.description ?? t.title),
|
|
5042
|
-
prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
|
|
5043
|
-
type: t.type === "user" ? "user" : "ai",
|
|
5044
|
-
status: planLabel(t.status, "pending"),
|
|
5045
|
-
dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
|
|
5046
|
-
assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
|
|
5047
|
-
taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
|
|
5048
|
-
assignedModel: t.assignedModel && typeof t.assignedModel === "object" ? {
|
|
5049
|
-
modelId: planLabel(t.assignedModel.modelId),
|
|
5050
|
-
modelLabel: planLabel(t.assignedModel.modelLabel ?? t.assignedModel.modelId),
|
|
5051
|
-
thinkingEffort: typeof t.assignedModel.thinkingEffort === "string" ? planLabel(t.assignedModel.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
|
|
5052
|
-
availableVariants: Array.isArray(t.assignedModel.availableVariants) ? t.assignedModel.availableVariants.map((variant) => planLabel(variant)) : void 0
|
|
5053
|
-
} : void 0,
|
|
5054
|
-
subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((s, i) => toTaskView2(s, i)) : void 0
|
|
5055
|
-
};
|
|
5056
|
-
}
|
|
5057
|
-
var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
|
|
5058
|
-
function applySettings(state, settings) {
|
|
5059
|
-
const sources = {
|
|
5060
|
-
tdd: settings.tdd,
|
|
5061
|
-
verify: settings.verification
|
|
5062
|
-
};
|
|
5063
|
-
const skills = { ...state.skills };
|
|
5064
|
-
for (const id of SKILL_IDS) {
|
|
5065
|
-
const flag2 = enabledFlag(sources[id]);
|
|
5066
|
-
if (flag2 !== void 0) skills[id] = flag2;
|
|
5067
|
-
}
|
|
5068
|
-
return {
|
|
5069
|
-
...state,
|
|
5070
|
-
skills,
|
|
5071
|
-
orchestratorModel: typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : state.orchestratorModel,
|
|
5072
|
-
plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
|
|
5073
|
-
plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
|
|
5074
|
-
maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
|
|
5075
|
-
allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
|
|
5076
|
-
};
|
|
5077
|
-
}
|
|
5078
|
-
function refillPicker(state, kinds) {
|
|
5079
|
-
const overlay = state.overlay;
|
|
5080
|
-
if (overlay?.kind !== "picker" || !kinds.includes(overlay.picker.action.kind)) return state;
|
|
5081
|
-
const items = pickerItemsFor(state, overlay.picker.action);
|
|
5082
|
-
const hint = overlay.picker.action.kind === "set-model" ? providerErrorHint(state) : overlay.picker.hint;
|
|
5083
|
-
return { ...state, overlay: { kind: "picker", picker: { ...overlay.picker, items, hint, index: 0 } } };
|
|
5084
|
-
}
|
|
5085
|
-
function modelDetail(m) {
|
|
5086
|
-
return [m.provider, m.pricing].filter(Boolean).join(" \xB7 ") || void 0;
|
|
5087
|
-
}
|
|
5088
|
-
function providerErrorHint(state) {
|
|
5089
|
-
const failed = Object.keys(state.providerErrors ?? {});
|
|
5090
|
-
if (failed.length === 0) return void 0;
|
|
5091
|
-
const names = failed.map((p) => import_core20.ALL_PROVIDERS[p]?.label ?? p);
|
|
5092
|
-
return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
|
|
5093
|
-
}
|
|
5094
|
-
function plannerModelItems(state) {
|
|
5095
|
-
const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
|
|
5096
|
-
if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
|
|
5097
|
-
const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
|
|
5098
|
-
id: m.id,
|
|
5099
|
-
label: m.label,
|
|
5100
|
-
detail: [
|
|
5101
|
-
m.provider,
|
|
5102
|
-
m.variants?.length ? `${m.variants.length} effort level${m.variants.length === 1 ? "" : "s"}` : "runner default effort"
|
|
5103
|
-
].filter(Boolean).join(" \xB7 "),
|
|
5104
|
-
selected: m.id === state.orchestratorModel
|
|
5105
|
-
}));
|
|
5106
|
-
return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
|
|
5107
|
-
}
|
|
5108
|
-
function plannerEffortItems(state) {
|
|
5109
|
-
const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
|
|
5110
|
-
if (!runner) {
|
|
5111
|
-
return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
|
|
5112
|
-
}
|
|
5113
|
-
const model = state.models.find((m) => m.id === state.orchestratorModel && m.runners?.includes(runner));
|
|
5114
|
-
if (!model) {
|
|
5115
|
-
return [{ id: "", label: "No planner model selected", detail: "pick one with /model", disabled: true }];
|
|
5116
|
-
}
|
|
5117
|
-
const variants = model.variants ?? [];
|
|
5118
|
-
if (variants.length === 0) {
|
|
5119
|
-
return [{ id: "", label: `${model.label} exposes no effort levels`, detail: "it always runs at the agent's default", disabled: true }];
|
|
5120
|
-
}
|
|
5121
|
-
return [
|
|
5122
|
-
{ id: DEFAULT_EFFORT, label: "Runner default", detail: "Let the agent choose", selected: !state.plannerEffort },
|
|
5123
|
-
...variants.map((v) => ({ id: v.id, label: v.label, selected: v.id === state.plannerEffort }))
|
|
5124
|
-
];
|
|
5061
|
+
return [
|
|
5062
|
+
{ id: DEFAULT_EFFORT, label: "Runner default", detail: "Let the agent choose", selected: !state.plannerEffort },
|
|
5063
|
+
...variants.map((v) => ({ id: v.id, label: v.label, selected: v.id === state.plannerEffort }))
|
|
5064
|
+
];
|
|
5125
5065
|
}
|
|
5126
5066
|
function pickerItemsFor(state, action) {
|
|
5127
5067
|
if (action.kind === "set-model") {
|
|
@@ -5168,7 +5108,7 @@ function pickerItemsFor(state, action) {
|
|
|
5168
5108
|
}));
|
|
5169
5109
|
}
|
|
5170
5110
|
if (action.kind === "set-task-deps") {
|
|
5171
|
-
return (0,
|
|
5111
|
+
return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
|
|
5172
5112
|
id: candidate.id,
|
|
5173
5113
|
label: `#${candidate.order} ${candidate.title}`,
|
|
5174
5114
|
detail: candidate.status === "completed" ? "already completed" : void 0
|
|
@@ -5213,6 +5153,29 @@ function pickerItemsFor(state, action) {
|
|
|
5213
5153
|
}
|
|
5214
5154
|
return [];
|
|
5215
5155
|
}
|
|
5156
|
+
function plannerItems(state) {
|
|
5157
|
+
const installed = new Set(state.runners.map((r) => r.id));
|
|
5158
|
+
const agents = import_core23.CLI_PROVIDERS.map((id) => {
|
|
5159
|
+
const runner = (0, import_core23.runnerForProvider)(id);
|
|
5160
|
+
const ready = installed.has(runner);
|
|
5161
|
+
return {
|
|
5162
|
+
id,
|
|
5163
|
+
label: import_core23.ALL_PROVIDERS[id].label,
|
|
5164
|
+
detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
|
|
5165
|
+
selected: state.plannerProvider === id,
|
|
5166
|
+
disabled: !ready
|
|
5167
|
+
};
|
|
5168
|
+
});
|
|
5169
|
+
const vendors = import_core23.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
|
|
5170
|
+
id,
|
|
5171
|
+
label: import_core23.ALL_PROVIDERS[id].label,
|
|
5172
|
+
detail: import_core23.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5173
|
+
selected: state.plannerProvider === id
|
|
5174
|
+
}));
|
|
5175
|
+
return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
// src/tui/reducers/pointer.ts
|
|
5216
5179
|
var WHEEL_NOTCH = 3;
|
|
5217
5180
|
function pageNotch(viewport) {
|
|
5218
5181
|
return Math.max(1, viewport - 1);
|
|
@@ -5225,6 +5188,7 @@ function scrollDelta(key, state) {
|
|
|
5225
5188
|
return null;
|
|
5226
5189
|
}
|
|
5227
5190
|
var isWheel = (key) => key.name === "scrollup" || key.name === "scrolldown";
|
|
5191
|
+
var isMouseSelect = (key) => key.name === "mousedown" || key.name === "mousedrag" || key.name === "mouseup";
|
|
5228
5192
|
function pointerPane(state, key) {
|
|
5229
5193
|
if (key.col === void 0 || planPaneWidth(state) === 0) return state.focus;
|
|
5230
5194
|
return key.col <= chatPaneWidth(state) ? "chat" : "plan";
|
|
@@ -5233,7 +5197,34 @@ function scrollPointed(state, key) {
|
|
|
5233
5197
|
const delta = key.name === "scrollup" ? WHEEL_NOTCH : -WHEEL_NOTCH;
|
|
5234
5198
|
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta, false) : scrollChat(state, delta);
|
|
5235
5199
|
}
|
|
5236
|
-
|
|
5200
|
+
function scrollChat(state, delta) {
|
|
5201
|
+
return step({ ...state, scroll: clamp(state.scroll + delta, chatScrollMax(state)) });
|
|
5202
|
+
}
|
|
5203
|
+
function toggleDetail(state) {
|
|
5204
|
+
const flipped = { ...state, detailAll: !state.detailAll };
|
|
5205
|
+
const bound = chatScrollMax(state);
|
|
5206
|
+
const expanded = chatScrollMax(flipped);
|
|
5207
|
+
const scroll = flipped.scroll === 0 ? 0 : clamp(flipped.scroll + expanded - bound, expanded);
|
|
5208
|
+
return step({ ...flipped, scroll });
|
|
5209
|
+
}
|
|
5210
|
+
function settlePlan(state) {
|
|
5211
|
+
if (planPaneWidth(state) === 0) return state;
|
|
5212
|
+
const layout = planScrollExtent(state);
|
|
5213
|
+
return { ...state, planScroll: revealOffset(layout, planOffset(layout, state.planScroll), layout.anchor) };
|
|
5214
|
+
}
|
|
5215
|
+
function scrollPlan(state, delta, paged) {
|
|
5216
|
+
const layout = planScrollExtent(state);
|
|
5217
|
+
const from = planOffset(layout, state.planScroll);
|
|
5218
|
+
const to = clamp(from - delta, layout.maxScroll);
|
|
5219
|
+
const scrolled = { ...state, planScroll: to };
|
|
5220
|
+
if (state.taskEditor || layout.rowSpans.length === 0) return step(scrolled);
|
|
5221
|
+
const back = delta > 0;
|
|
5222
|
+
if (!paged) {
|
|
5223
|
+
return step({ ...scrolled, selectedTask: rowNearestView(layout, to, state.selectedTask) });
|
|
5224
|
+
}
|
|
5225
|
+
if (to === from) return step({ ...scrolled, selectedTask: back ? 0 : layout.rowSpans.length - 1 });
|
|
5226
|
+
return step({ ...scrolled, selectedTask: back ? bottomRowInView(layout, to) : topRowInView(layout, to) });
|
|
5227
|
+
}
|
|
5237
5228
|
function selectionCell(state, pane, key) {
|
|
5238
5229
|
const { first, last } = paneColumns(state, pane);
|
|
5239
5230
|
return {
|
|
@@ -5258,393 +5249,358 @@ function handleMouseSelect(state, key) {
|
|
|
5258
5249
|
if (isClick(moved)) return step({ ...state, selection: null });
|
|
5259
5250
|
return step({ ...state, selection: null }, [{ type: "copyText", text: selectedText({ ...state, selection: moved }) }]);
|
|
5260
5251
|
}
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
function
|
|
5265
|
-
|
|
5266
|
-
}
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
const scrolled = { ...state, planScroll: to };
|
|
5277
|
-
if (state.taskEditor || layout.rowSpans.length === 0) return step(scrolled);
|
|
5278
|
-
const back = delta > 0;
|
|
5279
|
-
if (!paged) {
|
|
5280
|
-
return step({ ...scrolled, selectedTask: rowNearestView(layout, to, state.selectedTask) });
|
|
5281
|
-
}
|
|
5282
|
-
if (to === from) return step({ ...scrolled, selectedTask: back ? 0 : layout.rowSpans.length - 1 });
|
|
5283
|
-
return step({ ...scrolled, selectedTask: back ? bottomRowInView(layout, to) : topRowInView(layout, to) });
|
|
5284
|
-
}
|
|
5285
|
-
var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
|
|
5286
|
-
function stopPlanning(state, sessionId) {
|
|
5287
|
-
const dismissed = state.overlay?.kind === "approval" ? { ...state, overlay: null, pendingApprovals: [] } : state;
|
|
5288
|
-
return step(dismissed, [{ type: "cancelPlanning", sessionId }]);
|
|
5289
|
-
}
|
|
5290
|
-
function handleKey2(state, key) {
|
|
5291
|
-
if (key.name === "ctrl-c") return interrupt(state);
|
|
5292
|
-
if (key.name === "ctrl-d" && !state.editor.text && !state.overlay) {
|
|
5293
|
-
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
5294
|
-
}
|
|
5295
|
-
if (key.name === "ctrl-l") return step({ ...state, messages: [] });
|
|
5296
|
-
if (key.name === "escape" && plannerInFlight(state) && state.sessionId && (!state.overlay || state.overlay.kind === "approval")) {
|
|
5297
|
-
return stopPlanning(state, state.sessionId);
|
|
5298
|
-
}
|
|
5299
|
-
if (state.overlay) return handleOverlayKey(state, state.overlay, key);
|
|
5300
|
-
if (isWheel(key)) return scrollPointed(state, key);
|
|
5301
|
-
if (isMouseSelect(key)) return handleMouseSelect(state, key);
|
|
5302
|
-
if (key.name === "wheelignored") return step(state);
|
|
5303
|
-
if (key.name === "tab" && state.focus === "chat") {
|
|
5304
|
-
const token = activeToken(state.editor.text, state.editor.cursor);
|
|
5305
|
-
const matches = token ? tokenCompletions(token) : [];
|
|
5306
|
-
if (token && matches.length > 0) {
|
|
5307
|
-
const { text: full } = state.editor;
|
|
5308
|
-
const completed = `/${matches[0].name} `;
|
|
5309
|
-
const text = full.slice(0, token.start) + completed + full.slice(token.end);
|
|
5310
|
-
return step({ ...state, editor: { ...state.editor, text, cursor: token.start + completed.length } });
|
|
5252
|
+
|
|
5253
|
+
// src/tui/reducers/taskEdits.ts
|
|
5254
|
+
var import_plan_utils3 = require("@ordewell/core/plan-utils");
|
|
5255
|
+
function openTaskRunnerPicker(state, task) {
|
|
5256
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
|
|
5257
|
+
const action = { kind: "set-task-runner", taskId: task.id };
|
|
5258
|
+
return step(
|
|
5259
|
+
{
|
|
5260
|
+
...state,
|
|
5261
|
+
overlay: {
|
|
5262
|
+
kind: "picker",
|
|
5263
|
+
picker: picker(`Runner \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
|
|
5264
|
+
hint: "Changing the runner re-picks this task model, effort and mode for it."
|
|
5265
|
+
})
|
|
5266
|
+
}
|
|
5311
5267
|
}
|
|
5312
|
-
|
|
5313
|
-
if (key.name === "tab") {
|
|
5314
|
-
if (state.focus === "chat" && state.tasks.length === 0) return step(state);
|
|
5315
|
-
return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
|
|
5316
|
-
}
|
|
5317
|
-
if (state.focus === "plan") return handlePlanKey(state, key);
|
|
5318
|
-
if (key.name === "enter") return submit(state);
|
|
5319
|
-
if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
5320
|
-
const scroll = scrollDelta(key, state);
|
|
5321
|
-
if (scroll !== null) return scrollChat(state, scroll);
|
|
5322
|
-
const multilineEditor = state.editor.text.includes("\n");
|
|
5323
|
-
const room = chatEditorRoom(state.cols, true);
|
|
5324
|
-
return step({ ...state, editor: applyKey(state.editor, key, multilineEditor ? room : void 0) });
|
|
5325
|
-
}
|
|
5326
|
-
function interrupt(state) {
|
|
5327
|
-
if (state.overlay) return step({ ...state, overlay: null });
|
|
5328
|
-
if (state.editor.text) return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
5329
|
-
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
5330
|
-
}
|
|
5331
|
-
var PLAN_SHORTCUTS = {
|
|
5332
|
-
c: "cancel",
|
|
5333
|
-
f: "force-start",
|
|
5334
|
-
s: "skip"
|
|
5335
|
-
};
|
|
5336
|
-
var SPAWNS_RUNNER = ["force-start", "retry"];
|
|
5337
|
-
function taskActionEffect(state, sessionId, taskId, action) {
|
|
5338
|
-
const watch = SPAWNS_RUNNER.includes(action) && state.status !== "executing";
|
|
5339
|
-
return watch ? { type: "taskAction", sessionId, taskId, action, watch: true } : { type: "taskAction", sessionId, taskId, action };
|
|
5268
|
+
);
|
|
5340
5269
|
}
|
|
5341
|
-
function
|
|
5342
|
-
|
|
5270
|
+
function confirmRemoveTask(state, task) {
|
|
5271
|
+
const dependents = (0, import_plan_utils3.dependentsOf)(state.tasks, task.id);
|
|
5272
|
+
const named = dependents.map((t) => `#${t.order} ${t.title}`).join(", ");
|
|
5273
|
+
return step({
|
|
5274
|
+
...state,
|
|
5275
|
+
overlay: {
|
|
5276
|
+
kind: "confirm",
|
|
5277
|
+
title: `Remove #${task.order} ${task.title}?`,
|
|
5278
|
+
message: dependents.length > 0 ? `${dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`} on it and will lose that dependency: ${named}.` : "This cannot be undone.",
|
|
5279
|
+
action: { kind: "remove-task", taskId: task.id }
|
|
5280
|
+
}
|
|
5281
|
+
});
|
|
5343
5282
|
}
|
|
5344
|
-
function
|
|
5345
|
-
const
|
|
5346
|
-
|
|
5347
|
-
if (
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
const scroll = scrollDelta(key, state);
|
|
5358
|
-
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5359
|
-
if (key.name === "char" && key.char === "a") return addTask(state, "");
|
|
5360
|
-
const row = selectedPlanRow(state);
|
|
5361
|
-
if (!row) return step(state);
|
|
5362
|
-
const task = row.task;
|
|
5363
|
-
if (key.name === "enter" || key.name === "right") {
|
|
5364
|
-
const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
|
|
5365
|
-
if (hasSubtasks && state.expandedTaskId !== task.id) {
|
|
5366
|
-
return step(settlePlan({ ...state, expandedTaskId: task.id, taskEditor: null }));
|
|
5283
|
+
function openTaskDepsPicker(state, task) {
|
|
5284
|
+
const action = { kind: "set-task-deps", taskId: task.id };
|
|
5285
|
+
const items = pickerItemsFor(state, action);
|
|
5286
|
+
if (items.length === 0) return fail3(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
|
|
5287
|
+
return step({
|
|
5288
|
+
...state,
|
|
5289
|
+
overlay: {
|
|
5290
|
+
kind: "picker",
|
|
5291
|
+
picker: picker(`Depends on \xB7 #${task.order} ${task.title}`, items, action, {
|
|
5292
|
+
hint: "Only tasks earlier in the plan can be dependencies.",
|
|
5293
|
+
multi: true,
|
|
5294
|
+
chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
|
|
5295
|
+
})
|
|
5367
5296
|
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
if (!state.sessionId || key.name !== "char") return step(state);
|
|
5376
|
-
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
5377
|
-
const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
|
|
5378
|
-
if (action) {
|
|
5379
|
-
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
5380
|
-
}
|
|
5381
|
-
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
5382
|
-
if (key.char === "t") {
|
|
5383
|
-
return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
|
|
5384
|
-
}
|
|
5385
|
-
if (key.char === "x" && task.isolation?.state === "conflict") {
|
|
5386
|
-
return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
|
|
5297
|
+
});
|
|
5298
|
+
}
|
|
5299
|
+
function openTaskModePicker(state, task) {
|
|
5300
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor mode.");
|
|
5301
|
+
const modes = modesForTask(state.modesByRunner, task);
|
|
5302
|
+
if (modes.length === 0) {
|
|
5303
|
+
return fail3(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
|
|
5387
5304
|
}
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5305
|
+
const action = { kind: "set-task-mode", taskId: task.id };
|
|
5306
|
+
return step(
|
|
5307
|
+
{
|
|
5308
|
+
...state,
|
|
5309
|
+
overlay: {
|
|
5310
|
+
kind: "picker",
|
|
5311
|
+
picker: picker(`Mode \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
|
|
5312
|
+
hint: `Modes declared by ${task.assignedRunner}.`
|
|
5313
|
+
})
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
);
|
|
5394
5317
|
}
|
|
5395
|
-
function
|
|
5396
|
-
if (
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5318
|
+
function openTaskModelPicker(state, task) {
|
|
5319
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor model.");
|
|
5320
|
+
const action = { kind: "set-task-model", taskId: task.id };
|
|
5321
|
+
return step(
|
|
5322
|
+
{
|
|
5323
|
+
...state,
|
|
5324
|
+
overlay: {
|
|
5325
|
+
kind: "picker",
|
|
5326
|
+
picker: picker(`Model \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
|
|
5327
|
+
hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
|
|
5328
|
+
})
|
|
5329
|
+
}
|
|
5330
|
+
},
|
|
5331
|
+
[{ type: "loadModels" }]
|
|
5332
|
+
);
|
|
5401
5333
|
}
|
|
5402
|
-
function
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
const
|
|
5406
|
-
|
|
5407
|
-
|
|
5334
|
+
function openTaskEffortPicker(state, task) {
|
|
5335
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have a thinking effort.");
|
|
5336
|
+
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
5337
|
+
const action = { kind: "set-task-effort", taskId: task.id };
|
|
5338
|
+
return step(
|
|
5339
|
+
{
|
|
5340
|
+
...state,
|
|
5341
|
+
overlay: {
|
|
5342
|
+
kind: "picker",
|
|
5343
|
+
picker: picker(`Thinking effort \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
|
|
5344
|
+
hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
|
|
5345
|
+
})
|
|
5346
|
+
}
|
|
5347
|
+
},
|
|
5348
|
+
[{ type: "loadModels" }]
|
|
5349
|
+
);
|
|
5350
|
+
}
|
|
5351
|
+
function assignTaskModel(state, sessionId, task, model) {
|
|
5352
|
+
const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
|
|
5353
|
+
return step(state, [{
|
|
5408
5354
|
type: "updateTask",
|
|
5409
|
-
sessionId
|
|
5355
|
+
sessionId,
|
|
5410
5356
|
taskId: task.id,
|
|
5411
|
-
|
|
5412
|
-
|
|
5357
|
+
// JSON drops `undefined`; null is intentional here so changing models can
|
|
5358
|
+
// also clear a stale legacy top-level effort on the persisted task.
|
|
5359
|
+
changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
|
|
5360
|
+
message: `Task #${task.order} model set to ${model.label}.`
|
|
5413
5361
|
}]);
|
|
5414
5362
|
}
|
|
5415
|
-
function
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5363
|
+
function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
|
|
5364
|
+
return step(state, [{
|
|
5365
|
+
type: "updateTask",
|
|
5366
|
+
sessionId,
|
|
5367
|
+
taskId: task.id,
|
|
5368
|
+
changes: { assignedRunner: runner },
|
|
5369
|
+
message: `Task #${task.order} runner set to ${runnerLabel}.`
|
|
5370
|
+
}]);
|
|
5421
5371
|
}
|
|
5422
|
-
function
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5372
|
+
function assignTaskMode(state, sessionId, task, mode, modeLabel) {
|
|
5373
|
+
return step(state, [{
|
|
5374
|
+
type: "updateTask",
|
|
5375
|
+
sessionId,
|
|
5376
|
+
taskId: task.id,
|
|
5377
|
+
changes: { taskMode: mode },
|
|
5378
|
+
message: `Task #${task.order} mode set to ${modeLabel}.`
|
|
5379
|
+
}]);
|
|
5427
5380
|
}
|
|
5428
|
-
function
|
|
5429
|
-
const
|
|
5430
|
-
|
|
5431
|
-
|
|
5381
|
+
function assignTaskEffort(state, sessionId, task, thinkingEffort) {
|
|
5382
|
+
const assignedModel = task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0;
|
|
5383
|
+
return step(state, [{
|
|
5384
|
+
type: "updateTask",
|
|
5385
|
+
sessionId,
|
|
5386
|
+
taskId: task.id,
|
|
5387
|
+
changes: { assignedModel, thinkingEffort: thinkingEffort ?? null },
|
|
5388
|
+
message: `Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`
|
|
5389
|
+
}]);
|
|
5432
5390
|
}
|
|
5433
|
-
function
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5391
|
+
function taskModelCommand(state, args) {
|
|
5392
|
+
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
5393
|
+
const task = findTask(state.tasks, taskId);
|
|
5394
|
+
if (!args[1]) return openTaskModelPicker(state, task);
|
|
5395
|
+
const model = state.models.find((candidate) => candidate.id === args[1]) ?? {
|
|
5396
|
+
id: args[1],
|
|
5397
|
+
label: args[1],
|
|
5398
|
+
provider: "",
|
|
5399
|
+
variants: []
|
|
5400
|
+
};
|
|
5401
|
+
if (!runnerAccepts(task, model)) {
|
|
5402
|
+
return fail3(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
|
|
5403
|
+
}
|
|
5404
|
+
return assignTaskModel(state, sessionId, task, model);
|
|
5405
|
+
});
|
|
5441
5406
|
}
|
|
5442
|
-
function
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
return chooseBlocked(state, "cancel");
|
|
5451
|
-
}
|
|
5452
|
-
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
5453
|
-
if (overlay.kind === "prompt") return handlePromptKey(state, overlay, key);
|
|
5454
|
-
return handlePickerKey(state, overlay.picker, key);
|
|
5407
|
+
function taskRunnerCommand(state, args) {
|
|
5408
|
+
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
5409
|
+
const task = findTask(state.tasks, taskId);
|
|
5410
|
+
if (!args[1]) return openTaskRunnerPicker(state, task);
|
|
5411
|
+
const runner = state.runners.find((candidate) => candidate.id === args[1]);
|
|
5412
|
+
if (!runner) return fail3(state, `Unknown runner "${args[1]}".`);
|
|
5413
|
+
return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
|
|
5414
|
+
});
|
|
5455
5415
|
}
|
|
5456
|
-
function
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
return
|
|
5463
|
-
}
|
|
5464
|
-
const chosen = key.name === "enter" ? choice.index : key.name === "char" ? Number(key.char) - 1 : -1;
|
|
5465
|
-
const option = choice.options[chosen];
|
|
5466
|
-
if (!option) return step(state);
|
|
5467
|
-
return option.confirms ? runConfirm(state, overlay) : step({ ...state, overlay: null });
|
|
5416
|
+
function taskModeCommand(state, args) {
|
|
5417
|
+
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
5418
|
+
const task = findTask(state.tasks, taskId);
|
|
5419
|
+
if (!args[1]) return openTaskModePicker(state, task);
|
|
5420
|
+
const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
|
|
5421
|
+
if (!mode) return fail3(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
|
|
5422
|
+
return assignTaskMode(state, sessionId, task, mode.id, mode.label);
|
|
5423
|
+
});
|
|
5468
5424
|
}
|
|
5469
|
-
function
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
if (!
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5425
|
+
function taskEffortCommand(state, args) {
|
|
5426
|
+
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
5427
|
+
const task = findTask(state.tasks, taskId);
|
|
5428
|
+
if (!args[1]) return openTaskEffortPicker(state, task);
|
|
5429
|
+
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
5430
|
+
const effort = args[1].toLowerCase();
|
|
5431
|
+
const value = effort === "default" || effort === "none" ? void 0 : args[1];
|
|
5432
|
+
const supported = effortsForTask(state.models, task);
|
|
5433
|
+
if (value && supported.length > 0 && !supported.some((variant) => variant.id === value)) {
|
|
5434
|
+
return fail3(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
|
|
5435
|
+
}
|
|
5436
|
+
return assignTaskEffort(state, sessionId, task, value);
|
|
5437
|
+
});
|
|
5438
|
+
}
|
|
5439
|
+
function addTask(state, title) {
|
|
5440
|
+
if (!state.sessionId) {
|
|
5441
|
+
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
5478
5442
|
}
|
|
5479
|
-
if (
|
|
5480
|
-
return step({
|
|
5443
|
+
if (!title) {
|
|
5444
|
+
return step({
|
|
5445
|
+
...state,
|
|
5446
|
+
overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
|
|
5447
|
+
});
|
|
5481
5448
|
}
|
|
5482
|
-
|
|
5483
|
-
return withIdlePlanner(closed, (sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index }]));
|
|
5449
|
+
return step(state, [{ type: "addTask", sessionId: state.sessionId, title }]);
|
|
5484
5450
|
}
|
|
5485
|
-
function
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
scrolldown: WHEEL_NOTCH,
|
|
5493
|
-
scrollup: -WHEEL_NOTCH
|
|
5494
|
-
};
|
|
5495
|
-
const delta = move[key.name];
|
|
5496
|
-
if (delta === void 0) return step({ ...state, overlay: null });
|
|
5497
|
-
return step({ ...state, overlay: { kind: "help", scroll: clamp(scroll + delta, helpScrollMax(state)) } });
|
|
5451
|
+
function taskCommand(state, token, run) {
|
|
5452
|
+
return withSession(state, (sessionId) => {
|
|
5453
|
+
if (!token) return fail3(state, "Which task? Pass a task id or its number in the plan.");
|
|
5454
|
+
const taskId = resolveTaskId2(state, token);
|
|
5455
|
+
if (!taskId) return fail3(state, `No task matching "${token}" in the current plan.`);
|
|
5456
|
+
return run(sessionId, taskId);
|
|
5457
|
+
});
|
|
5498
5458
|
}
|
|
5499
|
-
function
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
}
|
|
5507
|
-
if (!state.sessionId) return fail3(closed, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
5508
|
-
return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
|
|
5459
|
+
function resolveTaskId2(state, token) {
|
|
5460
|
+
const byId = findTask(state.tasks, token);
|
|
5461
|
+
if (byId) return byId.id;
|
|
5462
|
+
const order = Number(token);
|
|
5463
|
+
if (Number.isInteger(order)) {
|
|
5464
|
+
const byOrder = state.tasks.find((t) => t.order === order);
|
|
5465
|
+
if (byOrder) return byOrder.id;
|
|
5509
5466
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5467
|
+
return null;
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
// src/tui/reducers/planPane.ts
|
|
5471
|
+
var PLAN_SHORTCUTS = {
|
|
5472
|
+
c: "cancel",
|
|
5473
|
+
f: "force-start",
|
|
5474
|
+
s: "skip"
|
|
5475
|
+
};
|
|
5476
|
+
var SPAWNS_RUNNER = ["force-start", "retry"];
|
|
5477
|
+
function taskActionEffect(state, sessionId, taskId, action) {
|
|
5478
|
+
const watch = SPAWNS_RUNNER.includes(action) && state.status !== "executing";
|
|
5479
|
+
return watch ? { type: "taskAction", sessionId, taskId, action, watch: true } : { type: "taskAction", sessionId, taskId, action };
|
|
5512
5480
|
}
|
|
5513
|
-
function
|
|
5514
|
-
|
|
5515
|
-
if (key.name === "paste") return value + (key.text ?? "").replace(/\n/g, " ");
|
|
5516
|
-
if (key.name === "backspace") return value.slice(0, -1);
|
|
5517
|
-
if (key.name === "ctrl-u") return "";
|
|
5518
|
-
return value;
|
|
5481
|
+
function markAction(task) {
|
|
5482
|
+
return task.status === "completed" ? "uncomplete" : "complete";
|
|
5519
5483
|
}
|
|
5520
|
-
function
|
|
5521
|
-
const
|
|
5522
|
-
|
|
5523
|
-
if (key.name === "
|
|
5524
|
-
|
|
5525
|
-
return
|
|
5484
|
+
function handlePlanKey(state, key) {
|
|
5485
|
+
const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
|
|
5486
|
+
if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
|
|
5487
|
+
if (key.name === "escape") {
|
|
5488
|
+
if (state.expandedTaskId) return step(settlePlan({ ...state, expandedTaskId: null }));
|
|
5489
|
+
return step({ ...state, focus: "chat" });
|
|
5526
5490
|
}
|
|
5527
|
-
if (
|
|
5528
|
-
|
|
5529
|
-
if (!item) return reopen(picker2);
|
|
5530
|
-
const chosen = picker2.chosen.includes(item.id) ? picker2.chosen.filter((id) => id !== item.id) : [...picker2.chosen, item.id];
|
|
5531
|
-
return reopen({ ...picker2, chosen });
|
|
5491
|
+
if (key.name === "up") {
|
|
5492
|
+
return step(settlePlan({ ...state, selectedTask: Math.max(0, state.selectedTask - 1) }));
|
|
5532
5493
|
}
|
|
5533
|
-
if (key.name === "
|
|
5534
|
-
|
|
5535
|
-
if (filter === picker2.filter) return reopen(picker2);
|
|
5536
|
-
return reopen({ ...picker2, filter, index: 0 });
|
|
5537
|
-
}
|
|
5538
|
-
function choose(state, picker2, item) {
|
|
5539
|
-
const closed = { ...state, overlay: null };
|
|
5540
|
-
if (picker2.action.kind === "isolation-blocked") return chooseBlocked(state, item?.id ?? "cancel");
|
|
5541
|
-
if (picker2.action.kind === "set-allowlist") {
|
|
5542
|
-
return step(closed, [{ type: "setAllowlist", runner: picker2.action.runner, modelIds: picker2.chosen }]);
|
|
5494
|
+
if (key.name === "down") {
|
|
5495
|
+
return step(settlePlan({ ...state, selectedTask: clampSelection(state.selectedTask + 1, planRows(state).length) }));
|
|
5543
5496
|
}
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5497
|
+
const scroll = scrollDelta(key, state);
|
|
5498
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5499
|
+
if (key.name === "char" && key.char === "a") return addTask(state, "");
|
|
5500
|
+
const row = selectedPlanRow(state);
|
|
5501
|
+
if (!row) return step(state);
|
|
5502
|
+
const task = row.task;
|
|
5503
|
+
if (key.name === "enter" || key.name === "right") {
|
|
5504
|
+
const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
|
|
5505
|
+
if (hasSubtasks && state.expandedTaskId !== task.id) {
|
|
5506
|
+
return step(settlePlan({ ...state, expandedTaskId: task.id, taskEditor: null }));
|
|
5507
|
+
}
|
|
5508
|
+
const text = task.prompt ?? task.description ?? task.title;
|
|
5509
|
+
return step(settlePlan({
|
|
5510
|
+
...state,
|
|
5511
|
+
expandedTaskId: task.id,
|
|
5512
|
+
taskEditor: { ...emptyEditor(), text, cursor: text.length }
|
|
5513
|
+
}));
|
|
5553
5514
|
}
|
|
5554
|
-
if (
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
sessionId: state.sessionId,
|
|
5561
|
-
taskId: task.id,
|
|
5562
|
-
changes: { dependencies: picker2.chosen },
|
|
5563
|
-
message: picker2.chosen.length > 0 ? `Task #${task.order} now depends on ${picker2.chosen.length} task${picker2.chosen.length === 1 ? "" : "s"}.` : `Task #${task.order} no longer depends on anything.`
|
|
5564
|
-
}]);
|
|
5515
|
+
if (!state.sessionId || key.name !== "char") return step(state);
|
|
5516
|
+
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
5517
|
+
if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
|
|
5518
|
+
const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
|
|
5519
|
+
if (action) {
|
|
5520
|
+
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
5565
5521
|
}
|
|
5566
|
-
if (
|
|
5567
|
-
if (
|
|
5568
|
-
return step(
|
|
5522
|
+
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
5523
|
+
if (key.char === "t") {
|
|
5524
|
+
return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
|
|
5569
5525
|
}
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
return step(closed, [{ type: "setModel", modelId: item.id }]);
|
|
5573
|
-
case "set-planner":
|
|
5574
|
-
return step(closed, [{ type: "setPlanner", provider: item.id }]);
|
|
5575
|
-
case "set-planner-effort":
|
|
5576
|
-
return step(closed, [{ type: "setPlannerEffort", effort: item.id === DEFAULT_EFFORT ? "" : item.id }]);
|
|
5577
|
-
case "load-session":
|
|
5578
|
-
return step(closed, [{ type: "loadSession", sessionId: item.id }]);
|
|
5579
|
-
case "rewind":
|
|
5580
|
-
return pickRewindTarget(state, Number(item.id));
|
|
5581
|
-
case "delete-session":
|
|
5582
|
-
return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
|
|
5583
|
-
case "set-key":
|
|
5584
|
-
return step({ ...state, overlay: keyPrompt(item.id) });
|
|
5585
|
-
case "choose-allowlist-runner": {
|
|
5586
|
-
const action = { kind: "set-allowlist", runner: item.id };
|
|
5587
|
-
const items = pickerItemsFor(state, action);
|
|
5588
|
-
if (items.length === 0) {
|
|
5589
|
-
return step(say(state, "error", `No models discovered for ${item.label}. Run /refresh, then try again.`));
|
|
5590
|
-
}
|
|
5591
|
-
return step({
|
|
5592
|
-
...state,
|
|
5593
|
-
overlay: {
|
|
5594
|
-
kind: "picker",
|
|
5595
|
-
picker: {
|
|
5596
|
-
title: `Models allowed for ${item.label}`,
|
|
5597
|
-
hint: "An empty selection lifts the restriction.",
|
|
5598
|
-
items,
|
|
5599
|
-
filter: "",
|
|
5600
|
-
index: 0,
|
|
5601
|
-
multi: true,
|
|
5602
|
-
// Ids this runner doesn't serve can be sitting in a settings file
|
|
5603
|
-
// written before the list was scoped; dropping them here means
|
|
5604
|
-
// confirming the picker also repairs the stored allowlist.
|
|
5605
|
-
chosen: (state.allowlist[item.id] ?? []).filter((id) => items.some((i) => i.id === id)),
|
|
5606
|
-
action
|
|
5607
|
-
}
|
|
5608
|
-
}
|
|
5609
|
-
});
|
|
5610
|
-
}
|
|
5611
|
-
case "set-task-runner": {
|
|
5612
|
-
const taskId = picker2.action.taskId;
|
|
5613
|
-
const task = findTask(state.tasks, taskId);
|
|
5614
|
-
if (!task || !state.sessionId) return step(closed);
|
|
5615
|
-
return assignTaskRunner(closed, state.sessionId, task, item.id, item.label);
|
|
5616
|
-
}
|
|
5617
|
-
case "set-task-mode": {
|
|
5618
|
-
const taskId = picker2.action.taskId;
|
|
5619
|
-
const task = findTask(state.tasks, taskId);
|
|
5620
|
-
if (!task || !state.sessionId) return step(closed);
|
|
5621
|
-
return assignTaskMode(closed, state.sessionId, task, item.id, item.label);
|
|
5622
|
-
}
|
|
5623
|
-
case "set-task-model": {
|
|
5624
|
-
const taskId = picker2.action.taskId;
|
|
5625
|
-
const task = findTask(state.tasks, taskId);
|
|
5626
|
-
const model = state.models.find((candidate) => candidate.id === item.id);
|
|
5627
|
-
if (!task || !model || !state.sessionId) return step(closed);
|
|
5628
|
-
return assignTaskModel(closed, state.sessionId, task, model);
|
|
5629
|
-
}
|
|
5630
|
-
case "set-task-effort": {
|
|
5631
|
-
const taskId = picker2.action.taskId;
|
|
5632
|
-
const task = findTask(state.tasks, taskId);
|
|
5633
|
-
if (!task || !state.sessionId) return step(closed);
|
|
5634
|
-
return assignTaskEffort(closed, state.sessionId, task, item.id === DEFAULT_EFFORT ? void 0 : item.id);
|
|
5635
|
-
}
|
|
5526
|
+
if (key.char === "x" && task.isolation?.state === "conflict") {
|
|
5527
|
+
return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
|
|
5636
5528
|
}
|
|
5529
|
+
if (key.char === "R") return openTaskRunnerPicker(state, task);
|
|
5530
|
+
if (key.char === "o") return openTaskModelPicker(state, task);
|
|
5531
|
+
if (key.char === "e") return openTaskEffortPicker(state, task);
|
|
5532
|
+
if (key.char === "M") return openTaskModePicker(state, task);
|
|
5533
|
+
if (key.char === "D") return openTaskDepsPicker(state, task);
|
|
5534
|
+
return step(state);
|
|
5637
5535
|
}
|
|
5638
|
-
function
|
|
5639
|
-
|
|
5640
|
-
if (
|
|
5641
|
-
const
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5536
|
+
function handleTaskEditKey(state, task, editor, key) {
|
|
5537
|
+
if (key.name === "enter") return commitTaskEdit(state, task, editor);
|
|
5538
|
+
if (key.name === "escape") return step(settlePlan({ ...state, expandedTaskId: null, taskEditor: null }));
|
|
5539
|
+
const scroll = scrollDelta(key, state);
|
|
5540
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5541
|
+
return step(settlePlan({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) }));
|
|
5542
|
+
}
|
|
5543
|
+
function commitTaskEdit(state, task, editor) {
|
|
5544
|
+
const collapsed = settlePlan({ ...state, expandedTaskId: null, taskEditor: null });
|
|
5545
|
+
const prompt = editor.text.trim();
|
|
5546
|
+
const original = task.prompt ?? task.description ?? task.title;
|
|
5547
|
+
if (!prompt || prompt === original || !state.sessionId) return step(collapsed);
|
|
5548
|
+
return step(collapsed, [{
|
|
5549
|
+
type: "updateTask",
|
|
5550
|
+
sessionId: state.sessionId,
|
|
5551
|
+
taskId: task.id,
|
|
5552
|
+
changes: { prompt },
|
|
5553
|
+
message: `Task #${task.order} prompt updated.`
|
|
5554
|
+
}]);
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
// src/tui/reducers/turnQueue.ts
|
|
5558
|
+
var import_core24 = require("@ordewell/core");
|
|
5559
|
+
function drainQueue(state, settled) {
|
|
5560
|
+
const next = (0, import_core24.drainNext)(state.queuedPrompts);
|
|
5561
|
+
const ended = disarmStop({ ...settled, stopRequested: false });
|
|
5562
|
+
if (!next) return step(ended);
|
|
5563
|
+
const spoken = say(ended, "user", next.text);
|
|
5564
|
+
const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: next.text } : null;
|
|
5565
|
+
return step(
|
|
5566
|
+
{ ...spoken, queuedPrompts: next.rest, status: "planning", busyLabel: "" },
|
|
5567
|
+
effect ? [effect] : []
|
|
5568
|
+
);
|
|
5569
|
+
}
|
|
5570
|
+
function disarmStop(state) {
|
|
5571
|
+
return state.stopArmed ? { ...state, stopArmed: false } : state;
|
|
5572
|
+
}
|
|
5573
|
+
var STOP_ARM_MS = 2e3;
|
|
5574
|
+
function stopPlanning(state, sessionId) {
|
|
5575
|
+
const dismissed = state.overlay?.kind === "approval" ? { ...state, overlay: null, pendingApprovals: [] } : state;
|
|
5576
|
+
return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
|
|
5577
|
+
}
|
|
5578
|
+
function queueToEditor(state) {
|
|
5579
|
+
const all = (0, import_core24.unsendAll)(state.queuedPrompts);
|
|
5580
|
+
if (!all) return state;
|
|
5581
|
+
return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
|
|
5582
|
+
}
|
|
5583
|
+
function unsendLatest(state) {
|
|
5584
|
+
const latest = (0, import_core24.unsendLatest)(state.queuedPrompts);
|
|
5585
|
+
if (!latest) return step(state);
|
|
5586
|
+
const text = (0, import_core24.aheadOfDraft)(latest.text, state.editor.text);
|
|
5587
|
+
return step({
|
|
5588
|
+
...state,
|
|
5589
|
+
queuedPrompts: latest.rest,
|
|
5590
|
+
stopArmed: false,
|
|
5591
|
+
editor: { ...state.editor, text, cursor: latest.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
5592
|
+
});
|
|
5593
|
+
}
|
|
5594
|
+
function plannerEscape(state) {
|
|
5595
|
+
if (!plannerInFlight(state) || !state.sessionId || state.overlay && state.overlay.kind !== "approval") return null;
|
|
5596
|
+
if (state.queuedPrompts.length > 0) return unsendLatest(state);
|
|
5597
|
+
if (state.stopArmed) return stopPlanning(state, state.sessionId);
|
|
5598
|
+
const arm = state.stopArmToken + 1;
|
|
5599
|
+
return step({ ...state, stopArmed: true, stopArmToken: arm }, [{ type: "disarmStop", afterMs: STOP_ARM_MS, arm }]);
|
|
5647
5600
|
}
|
|
5601
|
+
|
|
5602
|
+
// src/tui/reducers/commands.ts
|
|
5603
|
+
var KNOWN_PROVIDERS2 = Object.keys(import_core25.ALL_PROVIDERS);
|
|
5648
5604
|
function runCommand(state, { name, args }) {
|
|
5649
5605
|
if (!findCommand(name)) {
|
|
5650
5606
|
return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
|
|
@@ -5669,9 +5625,7 @@ function runCommand(state, { name, args }) {
|
|
|
5669
5625
|
case "stop":
|
|
5670
5626
|
return withSession(
|
|
5671
5627
|
state,
|
|
5672
|
-
(sessionId) => step(state, [
|
|
5673
|
-
state.status === "planning" || state.status === "researching" ? { type: "cancelPlanning", sessionId } : { type: "stopExecution", sessionId }
|
|
5674
|
-
])
|
|
5628
|
+
(sessionId) => plannerInFlight(state) ? stopPlanning(state, sessionId) : step(state, [{ type: "stopExecution", sessionId }])
|
|
5675
5629
|
);
|
|
5676
5630
|
case "model":
|
|
5677
5631
|
return setModel(state, args);
|
|
@@ -5757,55 +5711,12 @@ function runCommand(state, { name, args }) {
|
|
|
5757
5711
|
}
|
|
5758
5712
|
return fail3(state, `/${name} is not wired up yet.`);
|
|
5759
5713
|
}
|
|
5760
|
-
function picker(title, items, action, extra = {}) {
|
|
5761
|
-
return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
|
|
5762
|
-
}
|
|
5763
|
-
function withSession(state, run) {
|
|
5764
|
-
if (!state.sessionId) {
|
|
5765
|
-
return fail3(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
|
|
5766
|
-
}
|
|
5767
|
-
return run(state.sessionId);
|
|
5768
|
-
}
|
|
5769
5714
|
function withIdlePlanner(state, run) {
|
|
5770
5715
|
return withSession(
|
|
5771
5716
|
state,
|
|
5772
|
-
(sessionId) => state
|
|
5717
|
+
(sessionId) => plannerInFlight(state) ? fail3(state, "The planner is still answering \u2014 wait for its reply, or /stop it first.") : run(sessionId)
|
|
5773
5718
|
);
|
|
5774
5719
|
}
|
|
5775
|
-
function rewind(state, arg) {
|
|
5776
|
-
return withIdlePlanner(state, (sessionId) => {
|
|
5777
|
-
if (arg === void 0) {
|
|
5778
|
-
return step(
|
|
5779
|
-
{ ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "Rewinding forks the conversation from just before the chosen message; the original is kept and the tasks ride along." }) } },
|
|
5780
|
-
[{ type: "loadRewindTargets", sessionId }]
|
|
5781
|
-
);
|
|
5782
|
-
}
|
|
5783
|
-
if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
|
|
5784
|
-
return step(state, [{ type: "loadRewindTargets", sessionId, pick: Number(arg) }]);
|
|
5785
|
-
});
|
|
5786
|
-
}
|
|
5787
|
-
var REWIND_OPTIONS = [
|
|
5788
|
-
{ label: "Restore Conversation", confirms: true },
|
|
5789
|
-
{ label: "Never mind", confirms: false }
|
|
5790
|
-
];
|
|
5791
|
-
function pickRewindTarget(state, index) {
|
|
5792
|
-
return withIdlePlanner({ ...state, overlay: null }, () => {
|
|
5793
|
-
const target = state.rewindTargets?.find((t) => t.index === index);
|
|
5794
|
-
if (!target) return fail3({ ...state, overlay: null }, `No message ${index} to rewind to \u2014 /rewind alone lists them.`);
|
|
5795
|
-
return step({
|
|
5796
|
-
...state,
|
|
5797
|
-
overlay: {
|
|
5798
|
-
kind: "confirm",
|
|
5799
|
-
title: "Rewind",
|
|
5800
|
-
message: "Confirm you want to restore to the point before you sent this message:",
|
|
5801
|
-
quote: target.content,
|
|
5802
|
-
note: "The conversation will be forked.\nThe code will be unchanged.",
|
|
5803
|
-
action: { kind: "rewind", index },
|
|
5804
|
-
choice: { options: REWIND_OPTIONS, index: 0 }
|
|
5805
|
-
}
|
|
5806
|
-
});
|
|
5807
|
-
});
|
|
5808
|
-
}
|
|
5809
5720
|
function resolveToggle(arg, current) {
|
|
5810
5721
|
const value = arg?.toLowerCase();
|
|
5811
5722
|
if (value === "on") return true;
|
|
@@ -5819,7 +5730,7 @@ function toggleSkill(state, skill, arg) {
|
|
|
5819
5730
|
return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
|
|
5820
5731
|
}
|
|
5821
5732
|
function requestNewSession(state) {
|
|
5822
|
-
const hasContent = state.tasks.length > 0 || state.
|
|
5733
|
+
const hasContent = state.tasks.length > 0 || state.conversation.blocks.length > 0 || state.goal !== "" || state.status !== "idle";
|
|
5823
5734
|
if (!state.sessionId || !hasContent) return newSession(state);
|
|
5824
5735
|
return step({
|
|
5825
5736
|
...state,
|
|
@@ -5840,7 +5751,8 @@ function newSession(state) {
|
|
|
5840
5751
|
goal: "",
|
|
5841
5752
|
tasks: [],
|
|
5842
5753
|
planApproved: false,
|
|
5843
|
-
|
|
5754
|
+
conversation: import_core25.EMPTY_CONVERSATION,
|
|
5755
|
+
turnGate: import_core25.NO_TURN,
|
|
5844
5756
|
selectedTask: 0,
|
|
5845
5757
|
expandedTaskId: null,
|
|
5846
5758
|
taskEditor: null,
|
|
@@ -5848,16 +5760,17 @@ function newSession(state) {
|
|
|
5848
5760
|
planScroll: null,
|
|
5849
5761
|
status: "idle",
|
|
5850
5762
|
busyLabel: "",
|
|
5851
|
-
thinkingLine: "",
|
|
5852
5763
|
// Prompts belong to the session that raised them; the old planner is gone
|
|
5853
5764
|
// and its pending requests deny on their own timeout.
|
|
5854
5765
|
pendingApprovals: [],
|
|
5766
|
+
queuedPrompts: import_core25.EMPTY_HOLD,
|
|
5767
|
+
stopArmed: false,
|
|
5855
5768
|
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
5856
5769
|
}, closeEffects);
|
|
5857
5770
|
}
|
|
5858
5771
|
function setModel(state, args) {
|
|
5859
5772
|
if (args[0] === "set" && args[1]) {
|
|
5860
|
-
const runner = (0,
|
|
5773
|
+
const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
|
|
5861
5774
|
const known = runner ? modelsForRunner(state.models, runner) : [];
|
|
5862
5775
|
if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
|
|
5863
5776
|
return fail3(state, `${args[1]} was not discovered for ${runner}.`);
|
|
@@ -5877,27 +5790,6 @@ function setModel(state, args) {
|
|
|
5877
5790
|
[{ type: "loadModels" }]
|
|
5878
5791
|
);
|
|
5879
5792
|
}
|
|
5880
|
-
function plannerItems(state) {
|
|
5881
|
-
const installed = new Set(state.runners.map((r) => r.id));
|
|
5882
|
-
const agents = import_core20.CLI_PROVIDERS.map((id) => {
|
|
5883
|
-
const runner = (0, import_core20.runnerForProvider)(id);
|
|
5884
|
-
const ready = installed.has(runner);
|
|
5885
|
-
return {
|
|
5886
|
-
id,
|
|
5887
|
-
label: import_core20.ALL_PROVIDERS[id].label,
|
|
5888
|
-
detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
|
|
5889
|
-
selected: state.plannerProvider === id,
|
|
5890
|
-
disabled: !ready
|
|
5891
|
-
};
|
|
5892
|
-
});
|
|
5893
|
-
const vendors = import_core20.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
|
|
5894
|
-
id,
|
|
5895
|
-
label: import_core20.ALL_PROVIDERS[id].label,
|
|
5896
|
-
detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5897
|
-
selected: state.plannerProvider === id
|
|
5898
|
-
}));
|
|
5899
|
-
return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
|
|
5900
|
-
}
|
|
5901
5793
|
function setPlanner(state, args) {
|
|
5902
5794
|
if (args[0]) {
|
|
5903
5795
|
const id = args[0].toLowerCase();
|
|
@@ -5951,16 +5843,16 @@ function setKey(state, args) {
|
|
|
5951
5843
|
}
|
|
5952
5844
|
return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
|
|
5953
5845
|
}
|
|
5954
|
-
const items =
|
|
5846
|
+
const items = import_core25.PROVIDER_PRIORITY.map((id) => ({
|
|
5955
5847
|
id,
|
|
5956
|
-
label:
|
|
5957
|
-
detail:
|
|
5848
|
+
label: import_core25.ALL_PROVIDERS[id].label,
|
|
5849
|
+
detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5958
5850
|
selected: state.configuredProviders.includes(id)
|
|
5959
5851
|
}));
|
|
5960
5852
|
return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
|
|
5961
5853
|
}
|
|
5962
5854
|
function keyPrompt(provider) {
|
|
5963
|
-
const meta =
|
|
5855
|
+
const meta = import_core25.ALL_PROVIDERS[provider];
|
|
5964
5856
|
return {
|
|
5965
5857
|
kind: "prompt",
|
|
5966
5858
|
title: `${meta.label} API key`,
|
|
@@ -6002,279 +5894,675 @@ function allowlist(state, args) {
|
|
|
6002
5894
|
...state,
|
|
6003
5895
|
overlay: {
|
|
6004
5896
|
kind: "picker",
|
|
6005
|
-
picker: picker("Limit models for which runner?", items, { kind: "choose-allowlist-runner" })
|
|
5897
|
+
picker: picker("Limit models for which runner?", items, { kind: "choose-allowlist-runner" })
|
|
5898
|
+
}
|
|
5899
|
+
},
|
|
5900
|
+
// The model list the next picker is built from must be the runner's real
|
|
5901
|
+
// one; refresh it while the user is still choosing a runner.
|
|
5902
|
+
[{ type: "loadModels" }]
|
|
5903
|
+
);
|
|
5904
|
+
}
|
|
5905
|
+
return fail3(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
|
|
5906
|
+
}
|
|
5907
|
+
function describeAllowlist(ids) {
|
|
5908
|
+
if (!ids || ids.length === 0) return "no restriction";
|
|
5909
|
+
return `${ids.length} model${ids.length === 1 ? "" : "s"} allowed`;
|
|
5910
|
+
}
|
|
5911
|
+
function runners(state, args) {
|
|
5912
|
+
const [runner, arg] = args;
|
|
5913
|
+
if (runner) {
|
|
5914
|
+
const known = state.runners.find((r) => r.id === runner);
|
|
5915
|
+
const enabled = resolveToggle(arg, known?.enabled ?? true);
|
|
5916
|
+
if (enabled === null) return fail3(state, "Usage: /runners [<runner-id> on|off]");
|
|
5917
|
+
return step(state, [{ type: "setRunnerEnabled", runner, enabled }]);
|
|
5918
|
+
}
|
|
5919
|
+
const action = { kind: "set-runners" };
|
|
5920
|
+
const items = pickerItemsFor(state, action);
|
|
5921
|
+
return step({ ...state, overlay: {
|
|
5922
|
+
kind: "picker",
|
|
5923
|
+
picker: picker("Runners", items, action, {
|
|
5924
|
+
hint: "Enabled runners are the ones the planner may assign work to.",
|
|
5925
|
+
multi: true,
|
|
5926
|
+
chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
|
|
5927
|
+
})
|
|
5928
|
+
} });
|
|
5929
|
+
}
|
|
5930
|
+
function setMaxParallel(state, arg) {
|
|
5931
|
+
if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once \u2014 /parallel <n> changes it.`));
|
|
5932
|
+
const limit = (0, import_core25.parseMaxParallel)(arg);
|
|
5933
|
+
if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
|
|
5934
|
+
return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
|
|
5935
|
+
}
|
|
5936
|
+
function setAutonomous(state, arg) {
|
|
5937
|
+
const enabled = resolveToggle(arg, state.autonomous);
|
|
5938
|
+
if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
|
|
5939
|
+
return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
|
|
5940
|
+
}
|
|
5941
|
+
function setMouseCapture(state, arg) {
|
|
5942
|
+
const enabled = resolveToggle(arg, state.mouseCapture);
|
|
5943
|
+
if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
|
|
5944
|
+
const noted = say(
|
|
5945
|
+
// A selection made under capture is meaningless once the terminal owns the
|
|
5946
|
+
// mouse again, and its highlight would sit on screen with nothing able to
|
|
5947
|
+
// move it.
|
|
5948
|
+
{ ...state, mouseCapture: enabled, selection: null },
|
|
5949
|
+
"system",
|
|
5950
|
+
enabled ? "Mouse capture on \u2014 the wheel scrolls, and dragging selects within one pane and copies on release." : "Mouse capture off \u2014 the terminal's own drag-select is back, across both panes; scroll with pgup/pgdn."
|
|
5951
|
+
);
|
|
5952
|
+
return step(noted, [{ type: "setMouseCapture", enabled }]);
|
|
5953
|
+
}
|
|
5954
|
+
function rewind(state, arg) {
|
|
5955
|
+
return withIdlePlanner(state, (sessionId) => {
|
|
5956
|
+
if (arg === void 0) {
|
|
5957
|
+
return step(
|
|
5958
|
+
{ ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "Rewinding forks the conversation from just before the chosen message; the original is kept and the tasks ride along." }) } },
|
|
5959
|
+
[{ type: "loadRewindTargets", sessionId }]
|
|
5960
|
+
);
|
|
5961
|
+
}
|
|
5962
|
+
if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
|
|
5963
|
+
return step(state, [{ type: "loadRewindTargets", sessionId, pick: Number(arg) }]);
|
|
5964
|
+
});
|
|
5965
|
+
}
|
|
5966
|
+
var REWIND_OPTIONS = [
|
|
5967
|
+
{ label: "Restore Conversation", confirms: true },
|
|
5968
|
+
{ label: "Never mind", confirms: false }
|
|
5969
|
+
];
|
|
5970
|
+
function pickRewindTarget(state, index) {
|
|
5971
|
+
return withIdlePlanner({ ...state, overlay: null }, () => {
|
|
5972
|
+
const target = state.rewindTargets?.find((t) => t.index === index);
|
|
5973
|
+
if (!target) return fail3({ ...state, overlay: null }, `No message ${index} to rewind to \u2014 /rewind alone lists them.`);
|
|
5974
|
+
return step({
|
|
5975
|
+
...state,
|
|
5976
|
+
overlay: {
|
|
5977
|
+
kind: "confirm",
|
|
5978
|
+
title: "Rewind",
|
|
5979
|
+
message: "Confirm you want to restore to the point before you sent this message:",
|
|
5980
|
+
quote: target.content,
|
|
5981
|
+
note: "The conversation will be forked.\nThe code will be unchanged.",
|
|
5982
|
+
action: { kind: "rewind", index },
|
|
5983
|
+
choice: { options: REWIND_OPTIONS, index: 0 }
|
|
5984
|
+
}
|
|
5985
|
+
});
|
|
5986
|
+
});
|
|
5987
|
+
}
|
|
5988
|
+
|
|
5989
|
+
// src/tui/reducers/overlays.ts
|
|
5990
|
+
function handleOverlayKey(state, overlay, key) {
|
|
5991
|
+
if (overlay.kind === "help") return handleHelpKey(state, overlay.scroll ?? 0, key);
|
|
5992
|
+
if (key.name === "wheelignored") return step(state);
|
|
5993
|
+
if (overlay.kind === "handoff") return handleHandoffKey(state, overlay, key, bodyRows(state));
|
|
5994
|
+
if (isWheel(key) && overlay.kind !== "picker") return scrollPointed(state, key);
|
|
5995
|
+
if (overlay.kind === "approval") return handleApprovalKey(state, overlay, key);
|
|
5996
|
+
if (overlay.kind === "confirm") return handleConfirmKey(state, overlay, key);
|
|
5997
|
+
if (key.name === "escape" && overlay.kind === "picker" && overlay.picker.action.kind === "isolation-blocked") {
|
|
5998
|
+
return chooseBlocked(state, "cancel");
|
|
5999
|
+
}
|
|
6000
|
+
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
6001
|
+
if (overlay.kind === "prompt") return handlePromptKey(state, overlay, key);
|
|
6002
|
+
return handlePickerKey(state, overlay.picker, key);
|
|
6003
|
+
}
|
|
6004
|
+
function handleConfirmKey(state, overlay, key) {
|
|
6005
|
+
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
6006
|
+
const { choice } = overlay;
|
|
6007
|
+
if (!choice) return key.name === "enter" ? runConfirm(state, overlay) : step(state);
|
|
6008
|
+
if (key.name === "up" || key.name === "down") {
|
|
6009
|
+
const index = clamp(choice.index + (key.name === "down" ? 1 : -1), choice.options.length - 1);
|
|
6010
|
+
return step({ ...state, overlay: { ...overlay, choice: { ...choice, index } } });
|
|
6011
|
+
}
|
|
6012
|
+
const chosen = key.name === "enter" ? choice.index : key.name === "char" ? Number(key.char) - 1 : -1;
|
|
6013
|
+
const option = choice.options[chosen];
|
|
6014
|
+
if (!option) return step(state);
|
|
6015
|
+
return option.confirms ? runConfirm(state, overlay) : step({ ...state, overlay: null });
|
|
6016
|
+
}
|
|
6017
|
+
function runConfirm(state, overlay) {
|
|
6018
|
+
const closed = { ...state, overlay: null };
|
|
6019
|
+
if (overlay.action.kind === "new-session") return newSession(closed);
|
|
6020
|
+
if (overlay.action.kind === "remove-task") {
|
|
6021
|
+
if (!state.sessionId) return step(closed);
|
|
6022
|
+
return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
|
|
6023
|
+
}
|
|
6024
|
+
if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
|
|
6025
|
+
return confirmedHandoff(state, overlay.action.kind);
|
|
6026
|
+
}
|
|
6027
|
+
if (overlay.action.kind === "init-workspace") {
|
|
6028
|
+
return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
|
|
6029
|
+
}
|
|
6030
|
+
const { index } = overlay.action;
|
|
6031
|
+
return withIdlePlanner(closed, (sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index }]));
|
|
6032
|
+
}
|
|
6033
|
+
function handleHelpKey(state, scroll, key) {
|
|
6034
|
+
const page = pageNotch(bodyRows(state));
|
|
6035
|
+
const move = {
|
|
6036
|
+
down: 1,
|
|
6037
|
+
up: -1,
|
|
6038
|
+
pagedown: page,
|
|
6039
|
+
pageup: -page,
|
|
6040
|
+
scrolldown: WHEEL_NOTCH,
|
|
6041
|
+
scrollup: -WHEEL_NOTCH
|
|
6042
|
+
};
|
|
6043
|
+
const delta = move[key.name];
|
|
6044
|
+
if (delta === void 0) return step({ ...state, overlay: null });
|
|
6045
|
+
return step({ ...state, overlay: { kind: "help", scroll: clamp(scroll + delta, helpScrollMax(state)) } });
|
|
6046
|
+
}
|
|
6047
|
+
function handlePromptKey(state, overlay, key) {
|
|
6048
|
+
if (key.name === "enter") {
|
|
6049
|
+
const value2 = overlay.value.trim();
|
|
6050
|
+
const closed = { ...state, overlay: null };
|
|
6051
|
+
if (!value2) return step(closed);
|
|
6052
|
+
if (overlay.action.kind === "api-key") {
|
|
6053
|
+
return step(closed, [{ type: "setApiKey", provider: overlay.action.provider, key: value2 }]);
|
|
6054
|
+
}
|
|
6055
|
+
if (!state.sessionId) return fail3(closed, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
6056
|
+
return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
|
|
6057
|
+
}
|
|
6058
|
+
const value = editText(overlay.value, key);
|
|
6059
|
+
return step({ ...state, overlay: { ...overlay, value } });
|
|
6060
|
+
}
|
|
6061
|
+
function editText(value, key) {
|
|
6062
|
+
if (key.name === "char") return value + (key.char ?? "");
|
|
6063
|
+
if (key.name === "paste") return value + (key.text ?? "").replace(/\n/g, " ");
|
|
6064
|
+
if (key.name === "backspace") return value.slice(0, -1);
|
|
6065
|
+
if (key.name === "ctrl-u") return "";
|
|
6066
|
+
return value;
|
|
6067
|
+
}
|
|
6068
|
+
function handlePickerKey(state, picker2, key) {
|
|
6069
|
+
const items = visibleItems(picker2);
|
|
6070
|
+
const reopen = (next) => step({ ...state, overlay: { kind: "picker", picker: next } });
|
|
6071
|
+
if (key.name === "up" || key.name === "scrollup") return reopen({ ...picker2, index: Math.max(0, picker2.index - 1) });
|
|
6072
|
+
if (key.name === "down" || key.name === "scrolldown") {
|
|
6073
|
+
return reopen({ ...picker2, index: Math.min(items.length - 1, picker2.index + 1) });
|
|
6074
|
+
}
|
|
6075
|
+
if (picker2.multi && key.name === "char" && key.char === " ") {
|
|
6076
|
+
const item = items[picker2.index];
|
|
6077
|
+
if (!item) return reopen(picker2);
|
|
6078
|
+
const chosen = picker2.chosen.includes(item.id) ? picker2.chosen.filter((id) => id !== item.id) : [...picker2.chosen, item.id];
|
|
6079
|
+
return reopen({ ...picker2, chosen });
|
|
6080
|
+
}
|
|
6081
|
+
if (key.name === "enter") return choose(state, picker2, items[picker2.index]);
|
|
6082
|
+
const filter = editText(picker2.filter, key);
|
|
6083
|
+
if (filter === picker2.filter) return reopen(picker2);
|
|
6084
|
+
return reopen({ ...picker2, filter, index: 0 });
|
|
6085
|
+
}
|
|
6086
|
+
function choose(state, picker2, item) {
|
|
6087
|
+
const closed = { ...state, overlay: null };
|
|
6088
|
+
if (picker2.action.kind === "isolation-blocked") return chooseBlocked(state, item?.id ?? "cancel");
|
|
6089
|
+
if (picker2.action.kind === "set-allowlist") {
|
|
6090
|
+
return step(closed, [{ type: "setAllowlist", runner: picker2.action.runner, modelIds: picker2.chosen }]);
|
|
6091
|
+
}
|
|
6092
|
+
if (picker2.action.kind === "set-runners") {
|
|
6093
|
+
const changes = state.runners.filter((runner) => runner.enabled !== picker2.chosen.includes(runner.id)).map((runner) => ({ runner: runner.id, enabled: !runner.enabled }));
|
|
6094
|
+
if (changes.length === 0) return step(closed);
|
|
6095
|
+
const names = state.runners.filter((r) => picker2.chosen.includes(r.id)).map((r) => r.name);
|
|
6096
|
+
return step(closed, [{
|
|
6097
|
+
type: "setRunners",
|
|
6098
|
+
changes,
|
|
6099
|
+
message: names.length > 0 ? `Runners enabled: ${names.join(", ")}.` : "No runners enabled \u2014 the planner has nothing to assign work to."
|
|
6100
|
+
}]);
|
|
6101
|
+
}
|
|
6102
|
+
if (picker2.action.kind === "set-task-deps") {
|
|
6103
|
+
const taskId = picker2.action.taskId;
|
|
6104
|
+
const task = findTask(state.tasks, taskId);
|
|
6105
|
+
if (!task || !state.sessionId) return step(closed);
|
|
6106
|
+
return step(closed, [{
|
|
6107
|
+
type: "updateTask",
|
|
6108
|
+
sessionId: state.sessionId,
|
|
6109
|
+
taskId: task.id,
|
|
6110
|
+
changes: { dependencies: picker2.chosen },
|
|
6111
|
+
message: picker2.chosen.length > 0 ? `Task #${task.order} now depends on ${picker2.chosen.length} task${picker2.chosen.length === 1 ? "" : "s"}.` : `Task #${task.order} no longer depends on anything.`
|
|
6112
|
+
}]);
|
|
6113
|
+
}
|
|
6114
|
+
if (!item) return step(state);
|
|
6115
|
+
if (item.disabled) {
|
|
6116
|
+
return step(say(state, "error", item.detail ? `${item.label} \u2014 ${item.detail}` : `${item.label} is not available.`));
|
|
6117
|
+
}
|
|
6118
|
+
switch (picker2.action.kind) {
|
|
6119
|
+
case "set-model":
|
|
6120
|
+
return step(closed, [{ type: "setModel", modelId: item.id }]);
|
|
6121
|
+
case "set-planner":
|
|
6122
|
+
return step(closed, [{ type: "setPlanner", provider: item.id }]);
|
|
6123
|
+
case "set-planner-effort":
|
|
6124
|
+
return step(closed, [{ type: "setPlannerEffort", effort: item.id === DEFAULT_EFFORT ? "" : item.id }]);
|
|
6125
|
+
case "load-session":
|
|
6126
|
+
return step(closed, [{ type: "loadSession", sessionId: item.id }]);
|
|
6127
|
+
case "rewind":
|
|
6128
|
+
return pickRewindTarget(state, Number(item.id));
|
|
6129
|
+
case "delete-session":
|
|
6130
|
+
return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
|
|
6131
|
+
case "set-key":
|
|
6132
|
+
return step({ ...state, overlay: keyPrompt(item.id) });
|
|
6133
|
+
case "choose-allowlist-runner": {
|
|
6134
|
+
const action = { kind: "set-allowlist", runner: item.id };
|
|
6135
|
+
const items = pickerItemsFor(state, action);
|
|
6136
|
+
if (items.length === 0) {
|
|
6137
|
+
return step(say(state, "error", `No models discovered for ${item.label}. Run /refresh, then try again.`));
|
|
6138
|
+
}
|
|
6139
|
+
return step({
|
|
6140
|
+
...state,
|
|
6141
|
+
overlay: {
|
|
6142
|
+
kind: "picker",
|
|
6143
|
+
picker: {
|
|
6144
|
+
title: `Models allowed for ${item.label}`,
|
|
6145
|
+
hint: "An empty selection lifts the restriction.",
|
|
6146
|
+
items,
|
|
6147
|
+
filter: "",
|
|
6148
|
+
index: 0,
|
|
6149
|
+
multi: true,
|
|
6150
|
+
// Ids this runner doesn't serve can be sitting in a settings file
|
|
6151
|
+
// written before the list was scoped; dropping them here means
|
|
6152
|
+
// confirming the picker also repairs the stored allowlist.
|
|
6153
|
+
chosen: (state.allowlist[item.id] ?? []).filter((id) => items.some((i) => i.id === id)),
|
|
6154
|
+
action
|
|
6155
|
+
}
|
|
6006
6156
|
}
|
|
6007
|
-
}
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
}
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6157
|
+
});
|
|
6158
|
+
}
|
|
6159
|
+
case "set-task-runner": {
|
|
6160
|
+
const taskId = picker2.action.taskId;
|
|
6161
|
+
const task = findTask(state.tasks, taskId);
|
|
6162
|
+
if (!task || !state.sessionId) return step(closed);
|
|
6163
|
+
return assignTaskRunner(closed, state.sessionId, task, item.id, item.label);
|
|
6164
|
+
}
|
|
6165
|
+
case "set-task-mode": {
|
|
6166
|
+
const taskId = picker2.action.taskId;
|
|
6167
|
+
const task = findTask(state.tasks, taskId);
|
|
6168
|
+
if (!task || !state.sessionId) return step(closed);
|
|
6169
|
+
return assignTaskMode(closed, state.sessionId, task, item.id, item.label);
|
|
6170
|
+
}
|
|
6171
|
+
case "set-task-model": {
|
|
6172
|
+
const taskId = picker2.action.taskId;
|
|
6173
|
+
const task = findTask(state.tasks, taskId);
|
|
6174
|
+
const model = state.models.find((candidate) => candidate.id === item.id);
|
|
6175
|
+
if (!task || !model || !state.sessionId) return step(closed);
|
|
6176
|
+
return assignTaskModel(closed, state.sessionId, task, model);
|
|
6177
|
+
}
|
|
6178
|
+
case "set-task-effort": {
|
|
6179
|
+
const taskId = picker2.action.taskId;
|
|
6180
|
+
const task = findTask(state.tasks, taskId);
|
|
6181
|
+
if (!task || !state.sessionId) return step(closed);
|
|
6182
|
+
return assignTaskEffort(closed, state.sessionId, task, item.id === DEFAULT_EFFORT ? void 0 : item.id);
|
|
6183
|
+
}
|
|
6026
6184
|
}
|
|
6027
|
-
const action = { kind: "set-runners" };
|
|
6028
|
-
const items = pickerItemsFor(state, action);
|
|
6029
|
-
return step({ ...state, overlay: {
|
|
6030
|
-
kind: "picker",
|
|
6031
|
-
picker: picker("Runners", items, action, {
|
|
6032
|
-
hint: "Enabled runners are the ones the planner may assign work to.",
|
|
6033
|
-
multi: true,
|
|
6034
|
-
chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
|
|
6035
|
-
})
|
|
6036
|
-
} });
|
|
6037
|
-
}
|
|
6038
|
-
function setMaxParallel(state, arg) {
|
|
6039
|
-
if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once \u2014 /parallel <n> changes it.`));
|
|
6040
|
-
const limit = (0, import_core20.parseMaxParallel)(arg);
|
|
6041
|
-
if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
|
|
6042
|
-
return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
|
|
6043
|
-
}
|
|
6044
|
-
function setAutonomous(state, arg) {
|
|
6045
|
-
const enabled = resolveToggle(arg, state.autonomous);
|
|
6046
|
-
if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
|
|
6047
|
-
return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
|
|
6048
6185
|
}
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
{ ...state, mouseCapture: enabled, selection: null },
|
|
6057
|
-
"system",
|
|
6058
|
-
enabled ? "Mouse capture on \u2014 the wheel scrolls, and dragging selects within one pane and copies on release." : "Mouse capture off \u2014 the terminal's own drag-select is back, across both panes; scroll with pgup/pgdn."
|
|
6059
|
-
);
|
|
6060
|
-
return step(noted, [{ type: "setMouseCapture", enabled }]);
|
|
6186
|
+
|
|
6187
|
+
// src/tui/reducers/incoming.ts
|
|
6188
|
+
var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
|
|
6189
|
+
var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
|
|
6190
|
+
var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
|
|
6191
|
+
function isRecord(value) {
|
|
6192
|
+
return typeof value === "object" && value !== null;
|
|
6061
6193
|
}
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
const
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
picker: picker(`Runner \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
|
|
6071
|
-
hint: "Changing the runner re-picks this task model, effort and mode for it."
|
|
6072
|
-
})
|
|
6073
|
-
}
|
|
6194
|
+
var wireOrder = (value) => Number(asRecord(value).order ?? 0);
|
|
6195
|
+
function normalizeTasks(plan) {
|
|
6196
|
+
const source = plan;
|
|
6197
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6198
|
+
for (const list of [source?.pendingTasks, source?.executionLog]) {
|
|
6199
|
+
for (const task of list ?? []) {
|
|
6200
|
+
const record = asRecord(task);
|
|
6201
|
+
byId.set(String(record.id), record);
|
|
6074
6202
|
}
|
|
6075
|
-
|
|
6203
|
+
}
|
|
6204
|
+
const raw = byId.size > 0 ? [...byId.values()].sort((a, b) => wireOrder(a) - wireOrder(b)) : (source?.tasks ?? []).map(asRecord);
|
|
6205
|
+
return raw.map((t, i) => toTaskView2(t, i));
|
|
6076
6206
|
}
|
|
6077
|
-
function
|
|
6078
|
-
const
|
|
6079
|
-
const
|
|
6080
|
-
return
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6207
|
+
function toTaskView2(t, index) {
|
|
6208
|
+
const model = asRecord(t.assignedModel);
|
|
6209
|
+
const assigned = isRecord(t.assignedModel);
|
|
6210
|
+
return {
|
|
6211
|
+
id: String(t.id ?? `task-${index + 1}`),
|
|
6212
|
+
order: typeof t.order === "number" ? t.order : index + 1,
|
|
6213
|
+
title: planLabel(t.title, "Untitled task"),
|
|
6214
|
+
description: planText(t.description ?? t.title),
|
|
6215
|
+
prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
|
|
6216
|
+
type: t.type === "user" ? "user" : "ai",
|
|
6217
|
+
status: planLabel(t.status, "pending"),
|
|
6218
|
+
dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
|
|
6219
|
+
assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
|
|
6220
|
+
taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
|
|
6221
|
+
assignedModel: assigned ? {
|
|
6222
|
+
modelId: planLabel(model.modelId),
|
|
6223
|
+
modelLabel: planLabel(model.modelLabel ?? model.modelId),
|
|
6224
|
+
thinkingEffort: typeof model.thinkingEffort === "string" ? planLabel(model.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
|
|
6225
|
+
availableVariants: Array.isArray(model.availableVariants) ? model.availableVariants.map((variant) => planLabel(variant)) : void 0
|
|
6226
|
+
} : void 0,
|
|
6227
|
+
subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => wireOrder(a) - wireOrder(b)).map((s, i) => toTaskView2(asRecord(s), i)) : void 0
|
|
6228
|
+
};
|
|
6089
6229
|
}
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
const
|
|
6093
|
-
|
|
6094
|
-
|
|
6230
|
+
var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
|
|
6231
|
+
function applySettings(state, settings) {
|
|
6232
|
+
const sources = {
|
|
6233
|
+
tdd: settings.tdd,
|
|
6234
|
+
verify: settings.verification
|
|
6235
|
+
};
|
|
6236
|
+
const skills = { ...state.skills };
|
|
6237
|
+
for (const id of SKILL_IDS) {
|
|
6238
|
+
const flag2 = enabledFlag(sources[id]);
|
|
6239
|
+
if (flag2 !== void 0) skills[id] = flag2;
|
|
6240
|
+
}
|
|
6241
|
+
return {
|
|
6095
6242
|
...state,
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
}
|
|
6104
|
-
});
|
|
6243
|
+
skills,
|
|
6244
|
+
orchestratorModel: typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : state.orchestratorModel,
|
|
6245
|
+
plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
|
|
6246
|
+
plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
|
|
6247
|
+
maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
|
|
6248
|
+
allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
|
|
6249
|
+
};
|
|
6105
6250
|
}
|
|
6106
|
-
function
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6251
|
+
function runLabel(tasks) {
|
|
6252
|
+
const running = tasks.filter(isTaskRunning);
|
|
6253
|
+
if (running.length > 0) {
|
|
6254
|
+
const [first] = running;
|
|
6255
|
+
const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
|
|
6256
|
+
return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
|
|
6111
6257
|
}
|
|
6112
|
-
const
|
|
6113
|
-
return
|
|
6114
|
-
|
|
6115
|
-
...state,
|
|
6116
|
-
overlay: {
|
|
6117
|
-
kind: "picker",
|
|
6118
|
-
picker: picker(`Mode \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
|
|
6119
|
-
hint: `Modes declared by ${task.assignedRunner}.`
|
|
6120
|
-
})
|
|
6121
|
-
}
|
|
6122
|
-
}
|
|
6123
|
-
);
|
|
6124
|
-
}
|
|
6125
|
-
function openTaskModelPicker(state, task) {
|
|
6126
|
-
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor model.");
|
|
6127
|
-
const action = { kind: "set-task-model", taskId: task.id };
|
|
6128
|
-
return step(
|
|
6129
|
-
{
|
|
6130
|
-
...state,
|
|
6131
|
-
overlay: {
|
|
6132
|
-
kind: "picker",
|
|
6133
|
-
picker: picker(`Model \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
|
|
6134
|
-
hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
|
|
6135
|
-
})
|
|
6136
|
-
}
|
|
6137
|
-
},
|
|
6138
|
-
[{ type: "loadModels" }]
|
|
6139
|
-
);
|
|
6140
|
-
}
|
|
6141
|
-
function openTaskEffortPicker(state, task) {
|
|
6142
|
-
if (task.type !== "ai") return fail3(state, "Manual tasks do not have a thinking effort.");
|
|
6143
|
-
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
6144
|
-
const action = { kind: "set-task-effort", taskId: task.id };
|
|
6145
|
-
return step(
|
|
6146
|
-
{
|
|
6147
|
-
...state,
|
|
6148
|
-
overlay: {
|
|
6149
|
-
kind: "picker",
|
|
6150
|
-
picker: picker(`Thinking effort \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
|
|
6151
|
-
hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
|
|
6152
|
-
})
|
|
6153
|
-
}
|
|
6154
|
-
},
|
|
6155
|
-
[{ type: "loadModels" }]
|
|
6156
|
-
);
|
|
6157
|
-
}
|
|
6158
|
-
function assignTaskModel(state, sessionId, task, model) {
|
|
6159
|
-
const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
|
|
6160
|
-
return step(state, [{
|
|
6161
|
-
type: "updateTask",
|
|
6162
|
-
sessionId,
|
|
6163
|
-
taskId: task.id,
|
|
6164
|
-
// JSON drops `undefined`; null is intentional here so changing models can
|
|
6165
|
-
// also clear a stale legacy top-level effort on the persisted task.
|
|
6166
|
-
changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
|
|
6167
|
-
message: `Task #${task.order} model set to ${model.label}.`
|
|
6168
|
-
}]);
|
|
6258
|
+
const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
|
|
6259
|
+
if (waiting === 0) return "";
|
|
6260
|
+
return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
|
|
6169
6261
|
}
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
}]);
|
|
6262
|
+
var statusText = (text) => sanitize(text).replace(/\s+/g, " ").trim();
|
|
6263
|
+
function inFlight(blocks) {
|
|
6264
|
+
return blocks.flatMap((block) => {
|
|
6265
|
+
if (block.type === "tool") return block.status === "pending" ? [statusText(`${block.headline.name}(${block.headline.keyArg})`)] : [];
|
|
6266
|
+
if (block.type !== "subagent" || block.status !== "running") return [];
|
|
6267
|
+
return [statusText(`Agent(${block.brief})`), ...inFlight(block.children)];
|
|
6268
|
+
});
|
|
6178
6269
|
}
|
|
6179
|
-
function
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
}]);
|
|
6270
|
+
function turnActivity(state) {
|
|
6271
|
+
if (!plannerInFlight(state)) return state;
|
|
6272
|
+
const open = inFlight(state.conversation.blocks);
|
|
6273
|
+
const status = open.length > 0 ? "researching" : "planning";
|
|
6274
|
+
const newest = open.at(-1) ?? "";
|
|
6275
|
+
const busyLabel = open.length > 1 ? `${newest} (+${open.length - 1} more)` : newest;
|
|
6276
|
+
return status === state.status && busyLabel === state.busyLabel ? state : { ...state, status, busyLabel };
|
|
6187
6277
|
}
|
|
6188
|
-
function
|
|
6189
|
-
const
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
message: `Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`
|
|
6196
|
-
}]);
|
|
6278
|
+
function followSession(state, message) {
|
|
6279
|
+
const heard = turnActivity(hear(state, message));
|
|
6280
|
+
if (message.type === "approval_request") {
|
|
6281
|
+
const { id, kind, subject, scope, detail } = message;
|
|
6282
|
+
return enqueueApproval(heard, { id, kind, subject, scope, ...detail ? { detail } : {} });
|
|
6283
|
+
}
|
|
6284
|
+
return message.type === "approval_settled" ? dropApproval(heard, message.id) : heard;
|
|
6197
6285
|
}
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6286
|
+
|
|
6287
|
+
// src/tui/reducer.ts
|
|
6288
|
+
function reduce(state, action) {
|
|
6289
|
+
switch (action.type) {
|
|
6290
|
+
case "key":
|
|
6291
|
+
return handleKey2(state, action.key);
|
|
6292
|
+
// Isolation belongs to one session's plan, and task ids repeat across
|
|
6293
|
+
// sessions, so nothing of the last one's may carry over into this.
|
|
6294
|
+
case "sessionStarted":
|
|
6295
|
+
return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
|
|
6296
|
+
// The daemon registers a session only once planning succeeds; when it does
|
|
6297
|
+
// not, holding on to the id would send every next message to a session the
|
|
6298
|
+
// server never had.
|
|
6299
|
+
case "sessionCleared":
|
|
6300
|
+
return step({
|
|
6301
|
+
...clearIsolation(state),
|
|
6302
|
+
sessionId: null,
|
|
6303
|
+
goal: "",
|
|
6304
|
+
pendingApprovals: [],
|
|
6305
|
+
queuedPrompts: import_core26.EMPTY_HOLD,
|
|
6306
|
+
stopArmed: false,
|
|
6307
|
+
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
6308
|
+
});
|
|
6309
|
+
case "chatRestored":
|
|
6310
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6311
|
+
return step({ ...state, conversation: (0, import_core26.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
|
|
6312
|
+
case "planUpdated": {
|
|
6313
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6314
|
+
const isolation = isolationOfPlan(action.plan);
|
|
6315
|
+
const tasks = isolationForPlan(normalizeTasks(action.plan), isolation, state.tasks);
|
|
6316
|
+
const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
|
|
6317
|
+
const next = {
|
|
6318
|
+
...state,
|
|
6319
|
+
tasks,
|
|
6320
|
+
handoff: isolation ? isolation.handoff : state.handoff,
|
|
6321
|
+
status: plannerInFlight(state) ? "idle" : state.status,
|
|
6322
|
+
busyLabel: "",
|
|
6323
|
+
expandedTaskId,
|
|
6324
|
+
taskEditor: expandedTaskId !== null ? state.taskEditor : null
|
|
6325
|
+
};
|
|
6326
|
+
const selectedTask = clampSelection(state.selectedTask, planRows(next).length);
|
|
6327
|
+
const settled = settlePlan({
|
|
6328
|
+
...next,
|
|
6329
|
+
selectedTask,
|
|
6330
|
+
focus: tasks.length === 0 ? "chat" : state.focus
|
|
6331
|
+
});
|
|
6332
|
+
if (!plannerInFlight(state)) {
|
|
6333
|
+
return step(settled);
|
|
6334
|
+
}
|
|
6335
|
+
return drainQueue(state, settled);
|
|
6210
6336
|
}
|
|
6211
|
-
|
|
6212
|
-
|
|
6337
|
+
// A message never ends a turn: `planUpdated` does (see `converse`), so a
|
|
6338
|
+
// queued prompt drains onto a settled state. Until then the status only
|
|
6339
|
+
// says whether a call is out.
|
|
6340
|
+
case "sessionMessage":
|
|
6341
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6342
|
+
return step(followSession(state, action.message));
|
|
6343
|
+
// A settled transcript line, not a research step: nothing ever settles a
|
|
6344
|
+
// task's start, so as a step it stayed "⋯" forever and was counted into
|
|
6345
|
+
// the planner's next "(+N more)".
|
|
6346
|
+
case "taskStarted": {
|
|
6347
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6348
|
+
const existing = findTask(state.tasks, action.taskId);
|
|
6349
|
+
if (existing && isTaskRunning(existing)) return step(state);
|
|
6350
|
+
const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
|
|
6351
|
+
const spoken = say(state, "system", (0, import_core26.taskStartedNotice)(action.title, action.runner));
|
|
6352
|
+
return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
|
|
6353
|
+
}
|
|
6354
|
+
case "taskStatus": {
|
|
6355
|
+
if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
|
|
6356
|
+
const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status } : t);
|
|
6357
|
+
const status = runStatus(state, tasks);
|
|
6358
|
+
return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
|
|
6359
|
+
}
|
|
6360
|
+
case "tasksStatus": {
|
|
6361
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6362
|
+
const updates = action.updates;
|
|
6363
|
+
let changed = false;
|
|
6364
|
+
const tasks = state.tasks.map((t) => {
|
|
6365
|
+
const update = updates[t.id];
|
|
6366
|
+
if (update === void 0) return t;
|
|
6367
|
+
const idleSince = update.idleSince ?? null;
|
|
6368
|
+
const isolation = update.isolation ?? t.isolation;
|
|
6369
|
+
if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
|
|
6370
|
+
changed = true;
|
|
6371
|
+
return { ...t, status: update.status, idleSince, isolation };
|
|
6372
|
+
}
|
|
6373
|
+
return t;
|
|
6374
|
+
});
|
|
6375
|
+
if (!changed) return step(state);
|
|
6376
|
+
const status = runStatus(state, tasks);
|
|
6377
|
+
return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
|
|
6378
|
+
}
|
|
6379
|
+
case "queueReady": {
|
|
6380
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6381
|
+
const sessionId = action.sessionId ?? state.sessionId;
|
|
6382
|
+
if (sessionId === null) return step(state);
|
|
6383
|
+
return step(state, [{ type: "processQueued", sessionId }]);
|
|
6384
|
+
}
|
|
6385
|
+
case "isolationBlocked": {
|
|
6386
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6387
|
+
return step(blockedPicker(state, action.message, action.repos));
|
|
6388
|
+
}
|
|
6389
|
+
case "isolationHandoff": {
|
|
6390
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6391
|
+
return step(handoffArrived(state, action.handoff));
|
|
6392
|
+
}
|
|
6393
|
+
case "handoffDiff": {
|
|
6394
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6395
|
+
return step(showDiff(state, action.diff));
|
|
6396
|
+
}
|
|
6397
|
+
case "runCleared": {
|
|
6398
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6399
|
+
const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
|
|
6400
|
+
return step({ ...clearIsolation(state), overlay });
|
|
6401
|
+
}
|
|
6402
|
+
case "executionComplete": {
|
|
6403
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6404
|
+
const { completed, total, failed } = action.summary ?? {
|
|
6405
|
+
completed: state.tasks.filter((t) => t.status === "completed").length,
|
|
6406
|
+
total: state.tasks.length,
|
|
6407
|
+
failed: state.tasks.filter((t) => t.status === "failed").length
|
|
6408
|
+
};
|
|
6409
|
+
const failures = failed > 0 ? ` \xB7 ${failed} failed` : "";
|
|
6410
|
+
const verb = action.stopped ? "stopped" : "finished";
|
|
6411
|
+
return step({
|
|
6412
|
+
...say(state, "system", `Execution ${verb} \u2014 ${completed}/${total} tasks complete${failures}.`),
|
|
6413
|
+
status: "idle",
|
|
6414
|
+
busyLabel: ""
|
|
6415
|
+
});
|
|
6416
|
+
}
|
|
6417
|
+
case "settingsLoaded":
|
|
6418
|
+
return step(applySettings(state, action.settings));
|
|
6419
|
+
case "modelsLoaded":
|
|
6420
|
+
return step(
|
|
6421
|
+
refillPicker(
|
|
6422
|
+
{
|
|
6423
|
+
...state,
|
|
6424
|
+
models: action.models,
|
|
6425
|
+
orchestratorModels: action.orchestratorModels ?? state.orchestratorModels,
|
|
6426
|
+
configuredProviders: action.providers ?? state.configuredProviders,
|
|
6427
|
+
providerErrors: action.providerErrors ?? state.providerErrors,
|
|
6428
|
+
modesByRunner: action.modesByRunner ?? state.modesByRunner
|
|
6429
|
+
},
|
|
6430
|
+
["set-model", "set-planner", "set-task-model", "set-task-effort", "set-task-mode"]
|
|
6431
|
+
)
|
|
6432
|
+
);
|
|
6433
|
+
case "sessionsLoaded":
|
|
6434
|
+
return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
|
|
6435
|
+
case "rewindTargetsLoaded":
|
|
6436
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6437
|
+
if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
|
|
6438
|
+
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
6439
|
+
// A fork holds no run, whatever the session it came from was doing — the
|
|
6440
|
+
// original keeps its run, and its events are stale from here on.
|
|
6441
|
+
case "sessionForked":
|
|
6442
|
+
return step({
|
|
6443
|
+
...clearIsolation(state),
|
|
6444
|
+
sessionId: action.sessionId,
|
|
6445
|
+
goal: action.goal,
|
|
6446
|
+
status: "idle",
|
|
6447
|
+
busyLabel: "",
|
|
6448
|
+
planApproved: false,
|
|
6449
|
+
pendingApprovals: [],
|
|
6450
|
+
queuedPrompts: import_core26.EMPTY_HOLD,
|
|
6451
|
+
stopArmed: false,
|
|
6452
|
+
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
6453
|
+
});
|
|
6454
|
+
// The rewound message comes back as a draft to edit and resend, so it lands
|
|
6455
|
+
// as if just typed: history is not being browsed, and nothing is parked.
|
|
6456
|
+
case "inputPrefilled":
|
|
6457
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
6458
|
+
return step({
|
|
6459
|
+
...state,
|
|
6460
|
+
focus: "chat",
|
|
6461
|
+
editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
6462
|
+
});
|
|
6463
|
+
case "runnersLoaded":
|
|
6464
|
+
return step(refillPicker({
|
|
6465
|
+
...state,
|
|
6466
|
+
runners: action.runners,
|
|
6467
|
+
orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
|
|
6468
|
+
}, ["set-planner", "set-task-runner"]));
|
|
6469
|
+
case "failed": {
|
|
6470
|
+
const quiet = plannerInFlight(state) && state.stopRequested;
|
|
6471
|
+
const reported = { ...quiet ? state : say(state, "error", action.message), status: "idle", busyLabel: "" };
|
|
6472
|
+
if (!plannerInFlight(state)) return step(reported);
|
|
6473
|
+
return drainQueue(state, reported);
|
|
6474
|
+
}
|
|
6475
|
+
case "workspaceNeedsInit":
|
|
6476
|
+
return step({
|
|
6477
|
+
...state,
|
|
6478
|
+
status: "idle",
|
|
6479
|
+
busyLabel: "",
|
|
6480
|
+
overlay: {
|
|
6481
|
+
kind: "confirm",
|
|
6482
|
+
title: "Initialize new workspace?",
|
|
6483
|
+
message: `"${action.workspace}" isn't a recognized project yet (no .git or manifest found). Start a new ordewell workspace here?`,
|
|
6484
|
+
action: { kind: "init-workspace", goal: action.goal, workspace: action.workspace }
|
|
6485
|
+
}
|
|
6486
|
+
});
|
|
6487
|
+
case "notice":
|
|
6488
|
+
return step(say(state, action.level === "error" ? "error" : "system", action.message));
|
|
6489
|
+
case "resize": {
|
|
6490
|
+
const resized = { ...state, rows: action.rows, cols: action.cols, selection: null };
|
|
6491
|
+
return step(settlePlan({ ...resized, scroll: clamp(resized.scroll, chatScrollMax(resized)) }));
|
|
6492
|
+
}
|
|
6493
|
+
// Whether there is anything to animate is the app loop's call (it owns the
|
|
6494
|
+
// timer); a tick that arrives simply advances the frame.
|
|
6495
|
+
case "spinnerTick":
|
|
6496
|
+
return step({ ...state, spinnerFrame: (state.spinnerFrame + 1) % 10 });
|
|
6497
|
+
// The scheduled expiry of an armed stop; a token that is no longer the live
|
|
6498
|
+
// arm is a stale timer (the user disarmed and re-armed in between) and is
|
|
6499
|
+
// ignored, so it cannot cut a newer arm short.
|
|
6500
|
+
case "stopDisarmed":
|
|
6501
|
+
if (!state.stopArmed || action.arm !== state.stopArmToken) return step(state);
|
|
6502
|
+
return step(disarmStop(state));
|
|
6503
|
+
}
|
|
6213
6504
|
}
|
|
6214
|
-
function
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
const runner = state.runners.find((candidate) => candidate.id === args[1]);
|
|
6219
|
-
if (!runner) return fail3(state, `Unknown runner "${args[1]}".`);
|
|
6220
|
-
return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
|
|
6221
|
-
});
|
|
6505
|
+
function runStatus(state, tasks) {
|
|
6506
|
+
if (tasks.some(isTaskRunning)) return "executing";
|
|
6507
|
+
if (tasks.some((t) => t.status === "awaiting_user")) return "executing";
|
|
6508
|
+
return plannerInFlight(state) ? state.status : "idle";
|
|
6222
6509
|
}
|
|
6223
|
-
function
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
|
|
6228
|
-
if (!mode) return fail3(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
|
|
6229
|
-
return assignTaskMode(state, sessionId, task, mode.id, mode.label);
|
|
6230
|
-
});
|
|
6510
|
+
function interrupt(state) {
|
|
6511
|
+
if (state.overlay) return step({ ...state, overlay: null });
|
|
6512
|
+
if (state.editor.text) return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
6513
|
+
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
6231
6514
|
}
|
|
6232
|
-
function
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6515
|
+
function handleKey2(state, key) {
|
|
6516
|
+
if (state.stopArmed && key.name !== "escape") state = disarmStop(state);
|
|
6517
|
+
if (key.name === "ctrl-c") return interrupt(state);
|
|
6518
|
+
if (key.name === "ctrl-d" && !state.editor.text && !state.overlay) {
|
|
6519
|
+
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
6520
|
+
}
|
|
6521
|
+
if (key.name === "ctrl-l") return step({ ...state, conversation: wiped(state.conversation), scroll: 0 });
|
|
6522
|
+
if (key.name === "ctrl-o" && state.focus === "chat" && !state.overlay) return toggleDetail(state);
|
|
6523
|
+
if (key.name === "escape") {
|
|
6524
|
+
const stopped = plannerEscape(state);
|
|
6525
|
+
if (stopped) return stopped;
|
|
6526
|
+
}
|
|
6527
|
+
if (state.overlay) return handleOverlayKey(state, state.overlay, key);
|
|
6528
|
+
if (isWheel(key)) return scrollPointed(state, key);
|
|
6529
|
+
if (isMouseSelect(key)) return handleMouseSelect(state, key);
|
|
6530
|
+
if (key.name === "wheelignored") return step(state);
|
|
6531
|
+
if (key.name === "tab" && state.focus === "chat") {
|
|
6532
|
+
const token = activeToken(state.editor.text, state.editor.cursor);
|
|
6533
|
+
const matches = token ? tokenCompletions(token) : [];
|
|
6534
|
+
if (token && matches.length > 0) {
|
|
6535
|
+
const { text: full } = state.editor;
|
|
6536
|
+
const completed = `/${matches[0].name} `;
|
|
6537
|
+
const text = full.slice(0, token.start) + completed + full.slice(token.end);
|
|
6538
|
+
return step({ ...state, editor: { ...state.editor, text, cursor: token.start + completed.length } });
|
|
6242
6539
|
}
|
|
6243
|
-
return assignTaskEffort(state, sessionId, task, value);
|
|
6244
|
-
});
|
|
6245
|
-
}
|
|
6246
|
-
function clampSelection(index, rows) {
|
|
6247
|
-
return Math.max(0, Math.min(index, rows - 1));
|
|
6248
|
-
}
|
|
6249
|
-
function addTask(state, title) {
|
|
6250
|
-
if (!state.sessionId) {
|
|
6251
|
-
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
6252
6540
|
}
|
|
6253
|
-
if (
|
|
6254
|
-
return step(
|
|
6255
|
-
|
|
6256
|
-
overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
|
|
6257
|
-
});
|
|
6541
|
+
if (key.name === "tab") {
|
|
6542
|
+
if (state.focus === "chat" && state.tasks.length === 0) return step(state);
|
|
6543
|
+
return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
|
|
6258
6544
|
}
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
});
|
|
6545
|
+
if (state.focus === "plan") return handlePlanKey(state, key);
|
|
6546
|
+
if (key.name === "enter") return submit(state);
|
|
6547
|
+
if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
6548
|
+
const scroll = scrollDelta(key, state);
|
|
6549
|
+
if (scroll !== null) return scrollChat(state, scroll);
|
|
6550
|
+
const multilineEditor = state.editor.text.includes("\n");
|
|
6551
|
+
const room = chatEditorRoom(state.cols, true);
|
|
6552
|
+
return step({ ...state, editor: applyKey(state.editor, key, multilineEditor ? room : void 0) });
|
|
6268
6553
|
}
|
|
6269
|
-
function
|
|
6270
|
-
const
|
|
6271
|
-
if (
|
|
6272
|
-
const
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6554
|
+
function submit(state) {
|
|
6555
|
+
const text = state.editor.text.trim();
|
|
6556
|
+
if (!text) return step(state);
|
|
6557
|
+
const cleared = { ...state, editor: commit(state.editor) };
|
|
6558
|
+
const command = parseSlash(text);
|
|
6559
|
+
if (command && findCommand(command.name)?.source !== "skill") return runCommand(cleared, command);
|
|
6560
|
+
if (plannerInFlight(state)) {
|
|
6561
|
+
return step({ ...cleared, queuedPrompts: (0, import_core26.holdPrompt)(state.queuedPrompts, text) });
|
|
6276
6562
|
}
|
|
6277
|
-
|
|
6563
|
+
const spoken = say(cleared, "user", text);
|
|
6564
|
+
const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
|
|
6565
|
+
return step({ ...spoken, status: "planning", stopRequested: false }, [effect]);
|
|
6278
6566
|
}
|
|
6279
6567
|
|
|
6280
6568
|
// src/tui/app.ts
|
|
@@ -6330,40 +6618,286 @@ function createApp(deps) {
|
|
|
6330
6618
|
|
|
6331
6619
|
// src/tui/effects.ts
|
|
6332
6620
|
var import_child_process4 = require("child_process");
|
|
6333
|
-
var
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6621
|
+
var import_core28 = require("@ordewell/core");
|
|
6622
|
+
|
|
6623
|
+
// src/tui/inbound.ts
|
|
6624
|
+
var import_core27 = require("@ordewell/core");
|
|
6625
|
+
var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
|
|
6626
|
+
"planner_message",
|
|
6627
|
+
"planner_turn_started",
|
|
6628
|
+
"planner_turn_ended",
|
|
6629
|
+
"planner_text_delta",
|
|
6630
|
+
"planner_thinking_delta",
|
|
6631
|
+
"planner_text_retracted",
|
|
6632
|
+
"plan_token",
|
|
6633
|
+
"plan_generated",
|
|
6634
|
+
"research_step",
|
|
6635
|
+
"research_step_done",
|
|
6636
|
+
"subagent_started",
|
|
6637
|
+
"subagent_finished",
|
|
6638
|
+
"planner_usage",
|
|
6639
|
+
"planner_liveness",
|
|
6640
|
+
"approval_request",
|
|
6641
|
+
"approval_settled",
|
|
6642
|
+
"approval_decided"
|
|
6643
|
+
]);
|
|
6644
|
+
function isStreamed(event) {
|
|
6645
|
+
return event.type === "planner_text_delta" || event.type === "planner_thinking_delta" || event.type === "plan_token";
|
|
6646
|
+
}
|
|
6647
|
+
function isConversation(event) {
|
|
6648
|
+
return CONVERSATION_TYPES.has(event.type);
|
|
6649
|
+
}
|
|
6650
|
+
function joinStreamed(held, next) {
|
|
6651
|
+
if (held.type === "planner_text_delta" && next.type === "planner_text_delta") {
|
|
6652
|
+
return held.turnId === next.turnId && held.segmentId === next.segmentId ? { ...held, text: held.text + next.text } : null;
|
|
6653
|
+
}
|
|
6654
|
+
if (held.type === "planner_thinking_delta" && next.type === "planner_thinking_delta") {
|
|
6655
|
+
const same = held.turnId === next.turnId && held.segmentId === next.segmentId && held.subagentId === next.subagentId;
|
|
6656
|
+
return same ? { ...held, text: held.text + next.text } : null;
|
|
6657
|
+
}
|
|
6658
|
+
if (held.type === "plan_token" && next.type === "plan_token") {
|
|
6659
|
+
return held.turnId === next.turnId && held.segmentId === next.segmentId ? { ...held, token: held.token + next.token } : null;
|
|
6660
|
+
}
|
|
6661
|
+
return null;
|
|
6662
|
+
}
|
|
6663
|
+
function identity(event) {
|
|
6664
|
+
if (event.type === "planner_message") return replyKey(event.content, event.timestamp);
|
|
6665
|
+
if (event.type === "approval_request" || event.type === "approval_settled") return `${event.type}:${event.id}`;
|
|
6666
|
+
return JSON.stringify(event);
|
|
6667
|
+
}
|
|
6668
|
+
function replyKey(content, timestamp) {
|
|
6669
|
+
return timestamp ? `planner_message:${timestamp}:${content}` : `planner_message:content:${content}`;
|
|
6670
|
+
}
|
|
6671
|
+
var Copies = class {
|
|
6672
|
+
constructor(cap) {
|
|
6673
|
+
this.cap = cap;
|
|
6674
|
+
}
|
|
6675
|
+
cap;
|
|
6676
|
+
counts = /* @__PURE__ */ new Map();
|
|
6677
|
+
order = [];
|
|
6678
|
+
/** Whether this delivery is the first copy of a new broadcast, whichever subscription carried it. */
|
|
6679
|
+
first(id, key) {
|
|
6680
|
+
let highest = 0;
|
|
6681
|
+
for (const count of this.counts.get(key)?.values() ?? []) {
|
|
6682
|
+
if (count > highest) highest = count;
|
|
6683
|
+
}
|
|
6684
|
+
return this.bump(id, key) === highest + 1;
|
|
6685
|
+
}
|
|
6686
|
+
/** Account for a copy already shown (a redrawn transcript's entry), so its counterparts are recognized. */
|
|
6687
|
+
credit(id, key) {
|
|
6688
|
+
this.bump(id, key);
|
|
6689
|
+
}
|
|
6690
|
+
bump(id, key) {
|
|
6691
|
+
let keyed = this.counts.get(key);
|
|
6692
|
+
if (!keyed) {
|
|
6693
|
+
keyed = /* @__PURE__ */ new Map();
|
|
6694
|
+
this.counts.set(key, keyed);
|
|
6695
|
+
this.order.push(key);
|
|
6696
|
+
if (this.order.length > this.cap) this.counts.delete(this.order.shift());
|
|
6697
|
+
}
|
|
6698
|
+
const count = (keyed.get(id) ?? 0) + 1;
|
|
6699
|
+
keyed.set(id, count);
|
|
6700
|
+
return count;
|
|
6357
6701
|
}
|
|
6358
6702
|
};
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6703
|
+
var WindowSet = class {
|
|
6704
|
+
constructor(cap) {
|
|
6705
|
+
this.cap = cap;
|
|
6706
|
+
}
|
|
6707
|
+
cap;
|
|
6708
|
+
items = /* @__PURE__ */ new Set();
|
|
6709
|
+
order = [];
|
|
6710
|
+
add(value) {
|
|
6711
|
+
if (this.items.has(value)) return;
|
|
6712
|
+
this.items.add(value);
|
|
6713
|
+
this.order.push(value);
|
|
6714
|
+
if (this.order.length > this.cap) this.items.delete(this.order.shift());
|
|
6715
|
+
}
|
|
6716
|
+
has(value) {
|
|
6717
|
+
return this.items.has(value);
|
|
6718
|
+
}
|
|
6719
|
+
/** Removes the value and answers whether it was there. */
|
|
6720
|
+
take(value) {
|
|
6721
|
+
if (!this.items.has(value)) return false;
|
|
6722
|
+
this.items.delete(value);
|
|
6723
|
+
return true;
|
|
6724
|
+
}
|
|
6725
|
+
};
|
|
6726
|
+
var inbounds = /* @__PURE__ */ new WeakMap();
|
|
6727
|
+
function inboundFor(api, dispatch, sessionId) {
|
|
6728
|
+
let sessions = inbounds.get(api);
|
|
6729
|
+
if (!sessions) {
|
|
6730
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6731
|
+
inbounds.set(api, sessions);
|
|
6732
|
+
}
|
|
6733
|
+
let inbound = sessions.get(sessionId);
|
|
6734
|
+
if (!inbound) {
|
|
6735
|
+
inbound = createInbound(dispatch, sessionId);
|
|
6736
|
+
sessions.set(sessionId, inbound);
|
|
6737
|
+
}
|
|
6738
|
+
return inbound;
|
|
6739
|
+
}
|
|
6740
|
+
function createInbound(dispatch, sessionId) {
|
|
6741
|
+
const copies = new Copies(64);
|
|
6742
|
+
const shown = new WindowSet(64);
|
|
6743
|
+
const pending = new WindowSet(16);
|
|
6744
|
+
const DELTA_DEBOUNCE_MS = 75;
|
|
6745
|
+
let held = null;
|
|
6746
|
+
let timer = null;
|
|
6747
|
+
const flush = () => {
|
|
6748
|
+
if (timer !== null) {
|
|
6749
|
+
clearTimeout(timer);
|
|
6750
|
+
timer = null;
|
|
6751
|
+
}
|
|
6752
|
+
if (held === null) return;
|
|
6753
|
+
const message = held;
|
|
6754
|
+
held = null;
|
|
6755
|
+
dispatch({ type: "sessionMessage", message, sessionId });
|
|
6756
|
+
};
|
|
6757
|
+
const hold = (event) => {
|
|
6758
|
+
const joined = held && joinStreamed(held, event);
|
|
6759
|
+
if (joined) {
|
|
6760
|
+
held = joined;
|
|
6761
|
+
return;
|
|
6762
|
+
}
|
|
6763
|
+
flush();
|
|
6764
|
+
held = event;
|
|
6765
|
+
timer = setTimeout(flush, DELTA_DEBOUNCE_MS);
|
|
6766
|
+
};
|
|
6767
|
+
const showReply = (message) => {
|
|
6768
|
+
shown.add(message.content);
|
|
6769
|
+
dispatch({ type: "sessionMessage", message, sessionId });
|
|
6770
|
+
};
|
|
6771
|
+
const acceptReply = (id, event) => {
|
|
6772
|
+
const key = replyKey(event.content, event.timestamp);
|
|
6773
|
+
if (pending.take(key)) {
|
|
6774
|
+
copies.credit(id, key);
|
|
6775
|
+
return;
|
|
6776
|
+
}
|
|
6777
|
+
if (!copies.first(id, key)) return;
|
|
6778
|
+
showReply(event);
|
|
6779
|
+
};
|
|
6780
|
+
const deliverConversation = (kind, id, event) => {
|
|
6781
|
+
if (event.type === "planner_message") {
|
|
6782
|
+
acceptReply(id, event);
|
|
6783
|
+
return;
|
|
6784
|
+
}
|
|
6785
|
+
if (!copies.first(id, identity(event))) return;
|
|
6786
|
+
if (event.type === "plan_generated") {
|
|
6787
|
+
dispatch({ type: "sessionMessage", message: event, sessionId });
|
|
6788
|
+
if (kind === "execution") dispatch({ type: "planUpdated", plan: event.plan, sessionId });
|
|
6789
|
+
return;
|
|
6790
|
+
}
|
|
6791
|
+
dispatch({ type: "sessionMessage", message: event, sessionId });
|
|
6792
|
+
};
|
|
6793
|
+
const receive = (kind, id, event) => {
|
|
6794
|
+
if (isStreamed(event)) {
|
|
6795
|
+
if (kind === "planning") hold(event);
|
|
6796
|
+
return;
|
|
6797
|
+
}
|
|
6798
|
+
flush();
|
|
6799
|
+
if (isConversation(event)) {
|
|
6800
|
+
deliverConversation(kind, id, event);
|
|
6801
|
+
return;
|
|
6802
|
+
}
|
|
6803
|
+
if (!copies.first(id, identity(event))) return;
|
|
6804
|
+
dispatchLifecycle(dispatch, event, sessionId);
|
|
6805
|
+
};
|
|
6806
|
+
let subscription = 0;
|
|
6807
|
+
return {
|
|
6808
|
+
planning: () => {
|
|
6809
|
+
const id = `p${++subscription}`;
|
|
6810
|
+
return (event) => receive("planning", id, event);
|
|
6811
|
+
},
|
|
6812
|
+
execution: () => {
|
|
6813
|
+
const id = `e${++subscription}`;
|
|
6814
|
+
return (event) => receive("execution", id, event);
|
|
6815
|
+
},
|
|
6816
|
+
backfill: (content, timestamp) => {
|
|
6817
|
+
if (shown.has(content)) return;
|
|
6818
|
+
const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
6819
|
+
showReply(message);
|
|
6820
|
+
if (timestamp) pending.add(replyKey(content, timestamp));
|
|
6821
|
+
},
|
|
6822
|
+
redrawn: (content, timestamp) => {
|
|
6823
|
+
shown.add(content);
|
|
6824
|
+
pending.add(replyKey(content, timestamp));
|
|
6825
|
+
},
|
|
6826
|
+
flush
|
|
6364
6827
|
};
|
|
6828
|
+
}
|
|
6829
|
+
function dispatchLifecycle(dispatch, event, sessionId) {
|
|
6830
|
+
switch (event.type) {
|
|
6831
|
+
case "status_update": {
|
|
6832
|
+
const updates = {};
|
|
6833
|
+
for (const task of event.tasks ?? []) {
|
|
6834
|
+
updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
|
|
6835
|
+
}
|
|
6836
|
+
dispatch({ type: "tasksStatus", updates, sessionId });
|
|
6837
|
+
return;
|
|
6838
|
+
}
|
|
6839
|
+
case "task_started":
|
|
6840
|
+
dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
|
|
6841
|
+
return;
|
|
6842
|
+
// The extension shows these in its checkpoint panel; here they are
|
|
6843
|
+
// transcript lines, which is the TUI's equivalent surface.
|
|
6844
|
+
case "checkpoint":
|
|
6845
|
+
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core27.truncateCheckpointSummary)(event.summary)}` });
|
|
6846
|
+
return;
|
|
6847
|
+
case "review_needed":
|
|
6848
|
+
dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
|
|
6849
|
+
return;
|
|
6850
|
+
case "review_approved":
|
|
6851
|
+
dispatch({ type: "notice", message: "Plan approved." });
|
|
6852
|
+
return;
|
|
6853
|
+
// The orchestrator paused fan-out because a structural edit is queued; drain
|
|
6854
|
+
// it so the planner reconciles the plan and dependents resume spawning.
|
|
6855
|
+
// Dropped on the floor, the queue suppresses every later tick() — which is
|
|
6856
|
+
// exactly "tasks did not fan out when the dependent ones finished."
|
|
6857
|
+
case "queue_ready":
|
|
6858
|
+
dispatch({ type: "queueReady", sessionId });
|
|
6859
|
+
return;
|
|
6860
|
+
case "execution_complete":
|
|
6861
|
+
dispatch({ type: "executionComplete", summary: event.summary, sessionId });
|
|
6862
|
+
return;
|
|
6863
|
+
// A stop carries no tally — the reducer counts the pane instead.
|
|
6864
|
+
case "execution_stopped":
|
|
6865
|
+
dispatch({ type: "executionComplete", stopped: true, sessionId });
|
|
6866
|
+
return;
|
|
6867
|
+
// Nothing started; the user chooses how to go on.
|
|
6868
|
+
case "isolation_blocked":
|
|
6869
|
+
dispatch({ type: "isolationBlocked", message: event.message, ...event.repos ? { repos: event.repos } : {}, sessionId });
|
|
6870
|
+
return;
|
|
6871
|
+
// How the run isolates. The daemon has no toast channel, so this is the only place the user hears it.
|
|
6872
|
+
case "notice":
|
|
6873
|
+
dispatch({ type: "notice", message: event.message, level: event.level });
|
|
6874
|
+
return;
|
|
6875
|
+
case "isolation_handoff":
|
|
6876
|
+
dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
|
|
6877
|
+
return;
|
|
6878
|
+
// Raw runner chatter. Listed rather than defaulted, so a new lifecycle
|
|
6879
|
+
// variant fails to compile here until someone decides what a run's watcher
|
|
6880
|
+
// does with it.
|
|
6881
|
+
case "task_updated":
|
|
6882
|
+
case "task_output":
|
|
6883
|
+
return;
|
|
6884
|
+
// Its asker already has the words, from the merge request itself; a
|
|
6885
|
+
// viewer that did not ask still has to drop a run that is gone.
|
|
6886
|
+
case "isolation_merge":
|
|
6887
|
+
if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
|
|
6888
|
+
return;
|
|
6889
|
+
default: {
|
|
6890
|
+
const unhandled = event;
|
|
6891
|
+
void unhandled;
|
|
6892
|
+
return;
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
// src/tui/effects.ts
|
|
6898
|
+
async function runEffect(effect, deps) {
|
|
6365
6899
|
try {
|
|
6366
|
-
await
|
|
6900
|
+
await perform(effect, deps);
|
|
6367
6901
|
} catch (err) {
|
|
6368
6902
|
if (!isConnectionRefused(err)) {
|
|
6369
6903
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6382,7 +6916,7 @@ async function runEffect(effect, deps) {
|
|
|
6382
6916
|
}
|
|
6383
6917
|
deps.dispatch({ type: "notice", message: "The server had stopped; started a new one and retried." });
|
|
6384
6918
|
try {
|
|
6385
|
-
await
|
|
6919
|
+
await perform(effect, deps);
|
|
6386
6920
|
} catch (retryErr) {
|
|
6387
6921
|
const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
6388
6922
|
deps.dispatch({
|
|
@@ -6446,6 +6980,14 @@ async function perform(effect, deps) {
|
|
|
6446
6980
|
if (cancelled) dispatch({ type: "notice", message: "Planning stopped." });
|
|
6447
6981
|
return;
|
|
6448
6982
|
}
|
|
6983
|
+
// The arm's expiry is the runtime's to schedule: the reducer is pure, so
|
|
6984
|
+
// it names the delay and this owns the timer that ends the arming. Unref'd,
|
|
6985
|
+
// so a pending arm never holds the process open on its way out.
|
|
6986
|
+
case "disarmStop": {
|
|
6987
|
+
const timer = setTimeout(() => dispatch({ type: "stopDisarmed", arm: effect.arm }), effect.afterMs);
|
|
6988
|
+
timer.unref?.();
|
|
6989
|
+
return;
|
|
6990
|
+
}
|
|
6449
6991
|
case "processQueued":
|
|
6450
6992
|
await api.processQueued(effect.sessionId);
|
|
6451
6993
|
await refreshPlan(deps, effect.sessionId);
|
|
@@ -6501,7 +7043,7 @@ async function perform(effect, deps) {
|
|
|
6501
7043
|
dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
|
|
6502
7044
|
return;
|
|
6503
7045
|
case "setPlanner": {
|
|
6504
|
-
const meta =
|
|
7046
|
+
const meta = import_core28.ALL_PROVIDERS[effect.provider];
|
|
6505
7047
|
if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
|
|
6506
7048
|
const env = { AI_PROVIDER: effect.provider };
|
|
6507
7049
|
const settings = await api.updateSettings({ env });
|
|
@@ -6513,7 +7055,7 @@ async function perform(effect, deps) {
|
|
|
6513
7055
|
});
|
|
6514
7056
|
dispatch({
|
|
6515
7057
|
type: "notice",
|
|
6516
|
-
message: `${(0,
|
|
7058
|
+
message: `${(0, import_core28.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
|
|
6517
7059
|
});
|
|
6518
7060
|
await loadModels(deps);
|
|
6519
7061
|
return;
|
|
@@ -6525,7 +7067,7 @@ async function perform(effect, deps) {
|
|
|
6525
7067
|
dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
|
|
6526
7068
|
return;
|
|
6527
7069
|
case "setApiKey": {
|
|
6528
|
-
const meta =
|
|
7070
|
+
const meta = import_core28.ALL_PROVIDERS[effect.provider];
|
|
6529
7071
|
if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
|
|
6530
7072
|
await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
|
|
6531
7073
|
deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
|
|
@@ -6581,7 +7123,7 @@ async function perform(effect, deps) {
|
|
|
6581
7123
|
case "loadSession": {
|
|
6582
7124
|
const { plan, goal } = await api.adoptSession(effect.sessionId, workspace);
|
|
6583
7125
|
dispatch({ type: "sessionStarted", sessionId: effect.sessionId, goal });
|
|
6584
|
-
dispatch(
|
|
7126
|
+
dispatch(restoredChat(plan, effect.sessionId));
|
|
6585
7127
|
dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
|
|
6586
7128
|
dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
|
|
6587
7129
|
return;
|
|
@@ -6591,7 +7133,7 @@ async function perform(effect, deps) {
|
|
|
6591
7133
|
case "forkConversation": {
|
|
6592
7134
|
const fork = await api.forkConversation(effect.sessionId);
|
|
6593
7135
|
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6594
|
-
dispatch(
|
|
7136
|
+
dispatch(restoredChat(fork.plan, fork.sessionId));
|
|
6595
7137
|
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6596
7138
|
dispatch({ type: "notice", message: `Forked ${effect.sessionId} into ${fork.sessionId} \u2014 you are in the fork now. /sessions to go back.` });
|
|
6597
7139
|
return;
|
|
@@ -6610,17 +7152,22 @@ async function perform(effect, deps) {
|
|
|
6610
7152
|
case "rewindConversation": {
|
|
6611
7153
|
const fork = await api.rewindConversation(effect.sessionId, effect.index);
|
|
6612
7154
|
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6613
|
-
dispatch(
|
|
7155
|
+
dispatch(restoredChat(fork.plan, fork.sessionId));
|
|
6614
7156
|
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6615
7157
|
dispatch({ type: "inputPrefilled", text: fork.rewoundMessage, sessionId: fork.sessionId });
|
|
6616
7158
|
dispatch({ type: "notice", message: `Forked ${effect.sessionId} into ${fork.sessionId} from before that message \u2014 the original is kept (/sessions to go back). The message is ready to edit and resend.` });
|
|
6617
7159
|
return;
|
|
6618
7160
|
}
|
|
6619
7161
|
// The redrawn transcript opens with the summary entry, which is what the
|
|
6620
|
-
// user gets to read — no separate notice to repeat it.
|
|
7162
|
+
// user gets to read — no separate notice to repeat it. The daemon also
|
|
7163
|
+
// broadcast that summary; `redrawn` keeps a late socket copy of it from
|
|
7164
|
+
// landing as a second, spoken turn.
|
|
6621
7165
|
case "compactConversation": {
|
|
6622
7166
|
const { plan } = await api.compactConversation(effect.sessionId);
|
|
6623
|
-
|
|
7167
|
+
const inbound = inboundFor(deps.api, deps.dispatch, effect.sessionId);
|
|
7168
|
+
dispatch(restoredChat(plan, effect.sessionId));
|
|
7169
|
+
const summary = compactionSummary(plan);
|
|
7170
|
+
if (summary) inbound.redrawn(summary.content, summary.timestamp);
|
|
6624
7171
|
dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
|
|
6625
7172
|
return;
|
|
6626
7173
|
}
|
|
@@ -6691,7 +7238,7 @@ function osc52(text) {
|
|
|
6691
7238
|
function copySelection(deps, text) {
|
|
6692
7239
|
if (!text) return;
|
|
6693
7240
|
const lines = text.split("\n").length;
|
|
6694
|
-
const command = (0,
|
|
7241
|
+
const command = (0, import_core28.clipboardCopyCommand)(deps.hasBin);
|
|
6695
7242
|
if (command) {
|
|
6696
7243
|
try {
|
|
6697
7244
|
(deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
|
|
@@ -6712,194 +7259,52 @@ async function withExecutionStream(deps, sessionId, request) {
|
|
|
6712
7259
|
const streamReady = new Promise((resolve3, reject) => {
|
|
6713
7260
|
settleReady = (error) => error ? reject(error) : resolve3();
|
|
6714
7261
|
});
|
|
6715
|
-
const
|
|
6716
|
-
|
|
6717
|
-
(event) => onExecutionEvent(deps.dispatch, event, sessionId),
|
|
6718
|
-
settleReady
|
|
6719
|
-
);
|
|
7262
|
+
const inbound = inboundFor(deps.api, deps.dispatch, sessionId);
|
|
7263
|
+
const stream = deps.api.streamExecution(sessionId, inbound.execution(), settleReady);
|
|
6720
7264
|
void stream.catch(settleReady);
|
|
6721
7265
|
await streamReady;
|
|
6722
7266
|
await request();
|
|
6723
7267
|
await stream;
|
|
6724
7268
|
}
|
|
6725
7269
|
async function converse(deps, sessionId, call) {
|
|
6726
|
-
|
|
6727
|
-
const
|
|
6728
|
-
let tokenBuffer = "";
|
|
6729
|
-
let tokenTimer = null;
|
|
6730
|
-
const flushTokens = () => {
|
|
6731
|
-
if (tokenTimer !== null) {
|
|
6732
|
-
clearTimeout(tokenTimer);
|
|
6733
|
-
tokenTimer = null;
|
|
6734
|
-
}
|
|
6735
|
-
if (!tokenBuffer) return;
|
|
6736
|
-
const text = tokenBuffer;
|
|
6737
|
-
tokenBuffer = "";
|
|
6738
|
-
deps.dispatch({ type: "plannerToken", text, sessionId });
|
|
6739
|
-
};
|
|
6740
|
-
const stream = deps.api.streamPlanning(sessionId, (event) => {
|
|
6741
|
-
if (event?.type === "plan_token" && event.token) {
|
|
6742
|
-
tokenBuffer += event.token;
|
|
6743
|
-
if (tokenTimer === null) {
|
|
6744
|
-
tokenTimer = setTimeout(flushTokens, TOKEN_DEBOUNCE_MS);
|
|
6745
|
-
}
|
|
6746
|
-
return;
|
|
6747
|
-
}
|
|
6748
|
-
flushTokens();
|
|
6749
|
-
if (event?.type === "approval_request") {
|
|
6750
|
-
deps.dispatch({
|
|
6751
|
-
type: "approvalRequested",
|
|
6752
|
-
sessionId,
|
|
6753
|
-
request: { id: event.id, kind: event.kind, subject: event.subject, scope: event.scope, detail: event.detail }
|
|
6754
|
-
});
|
|
6755
|
-
return;
|
|
6756
|
-
}
|
|
6757
|
-
if (event?.type === "approval_settled") {
|
|
6758
|
-
deps.dispatch({ type: "approvalSettled", sessionId, approvalId: event.id });
|
|
6759
|
-
return;
|
|
6760
|
-
}
|
|
6761
|
-
if (event?.type === "approval_decided") {
|
|
6762
|
-
deps.dispatch({ type: "notice", message: describeApprovalDecision2(event) });
|
|
6763
|
-
return;
|
|
6764
|
-
}
|
|
6765
|
-
if (event?.type === "research_step") {
|
|
6766
|
-
const summary = (0, import_plan_utils4.summarizeToolCall)(event.tool, event.args || "{}", event.toolLabel);
|
|
6767
|
-
deps.dispatch({
|
|
6768
|
-
type: "researchStep",
|
|
6769
|
-
summary: event.subagentId ? `\u21B3 ${summary}` : summary,
|
|
6770
|
-
toolCallId: event.toolCallId,
|
|
6771
|
-
sessionId
|
|
6772
|
-
});
|
|
6773
|
-
return;
|
|
6774
|
-
}
|
|
6775
|
-
if (event?.type === "research_step_done" && event.step) {
|
|
6776
|
-
const step2 = event.step;
|
|
6777
|
-
const summary = (0, import_plan_utils4.summarizeToolCall)(step2.tool, step2.args, step2.toolLabel);
|
|
6778
|
-
deps.dispatch({
|
|
6779
|
-
type: "researchStepDone",
|
|
6780
|
-
summary: event.subagentId ? `\u21B3 ${summary}` : summary,
|
|
6781
|
-
toolCallId: step2.toolCallId,
|
|
6782
|
-
outcome: step2.outcome,
|
|
6783
|
-
result: step2.result ?? "",
|
|
6784
|
-
sessionId
|
|
6785
|
-
});
|
|
6786
|
-
return;
|
|
6787
|
-
}
|
|
6788
|
-
if (event?.type === "planner_message") {
|
|
6789
|
-
streamed = String(event.content ?? "");
|
|
6790
|
-
deps.dispatch({ type: "plannerMessage", content: streamed, sessionId });
|
|
6791
|
-
return;
|
|
6792
|
-
}
|
|
6793
|
-
if (event?.type === "plan_thinking" && event.text) {
|
|
6794
|
-
deps.dispatch({ type: "plannerThinking", text: event.text, sessionId });
|
|
6795
|
-
}
|
|
6796
|
-
});
|
|
7270
|
+
const inbound = inboundFor(deps.api, deps.dispatch, sessionId);
|
|
7271
|
+
const stream = deps.api.streamPlanning(sessionId, inbound.planning());
|
|
6797
7272
|
try {
|
|
7273
|
+
await stream.ready;
|
|
6798
7274
|
const plan = await call();
|
|
7275
|
+
inbound.flush();
|
|
7276
|
+
const reply = lastAssistantMessage(plan);
|
|
7277
|
+
if (reply) inbound.backfill(reply.content, reply.timestamp);
|
|
6799
7278
|
deps.dispatch({ type: "planUpdated", plan, sessionId });
|
|
6800
|
-
const question = lastAssistantMessage(plan);
|
|
6801
|
-
if (question && question !== streamed) {
|
|
6802
|
-
deps.dispatch({ type: "plannerMessage", content: question, sessionId });
|
|
6803
|
-
}
|
|
6804
7279
|
} finally {
|
|
6805
|
-
|
|
7280
|
+
inbound.flush();
|
|
6806
7281
|
stream.close();
|
|
6807
7282
|
}
|
|
6808
7283
|
}
|
|
6809
|
-
function
|
|
6810
|
-
const
|
|
6811
|
-
return
|
|
7284
|
+
function restoredChat(plan, sessionId) {
|
|
7285
|
+
const saved = plan;
|
|
7286
|
+
return {
|
|
7287
|
+
type: "chatRestored",
|
|
7288
|
+
history: saved?.conversationHistory ?? [],
|
|
7289
|
+
...saved?.researchLog ? { researchLog: saved.researchLog } : {},
|
|
7290
|
+
...saved?.plannerUsage ? { plannerUsage: saved.plannerUsage } : {},
|
|
7291
|
+
sessionId
|
|
7292
|
+
};
|
|
6812
7293
|
}
|
|
6813
7294
|
function lastAssistantMessage(plan) {
|
|
6814
7295
|
const history = plan?.conversationHistory ?? [];
|
|
6815
7296
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
6816
|
-
if (history[i].role === "assistant")
|
|
7297
|
+
if (history[i].role === "assistant") {
|
|
7298
|
+
const { content, timestamp } = history[i];
|
|
7299
|
+
return { content, ...timestamp ? { timestamp } : {} };
|
|
7300
|
+
}
|
|
6817
7301
|
}
|
|
6818
7302
|
return null;
|
|
6819
7303
|
}
|
|
6820
|
-
function
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
for (const task of event.tasks ?? []) {
|
|
6825
|
-
updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
|
|
6826
|
-
}
|
|
6827
|
-
dispatch({ type: "tasksStatus", updates, sessionId });
|
|
6828
|
-
return;
|
|
6829
|
-
}
|
|
6830
|
-
case "task_started":
|
|
6831
|
-
dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
|
|
6832
|
-
return;
|
|
6833
|
-
// The extension shows these in its checkpoint panel; here they are
|
|
6834
|
-
// transcript lines, which is the TUI's equivalent surface.
|
|
6835
|
-
case "checkpoint":
|
|
6836
|
-
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core21.truncateCheckpointSummary)(event.summary)}` });
|
|
6837
|
-
return;
|
|
6838
|
-
case "approval_decided":
|
|
6839
|
-
dispatch({ type: "notice", message: describeApprovalDecision2(event) });
|
|
6840
|
-
return;
|
|
6841
|
-
case "review_needed":
|
|
6842
|
-
dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
|
|
6843
|
-
return;
|
|
6844
|
-
case "review_approved":
|
|
6845
|
-
dispatch({ type: "notice", message: "Plan approved." });
|
|
6846
|
-
return;
|
|
6847
|
-
case "plan_generated":
|
|
6848
|
-
dispatch({ type: "planUpdated", plan: event.plan, sessionId });
|
|
6849
|
-
return;
|
|
6850
|
-
case "planner_message":
|
|
6851
|
-
dispatch({ type: "plannerMessage", content: String(event.content ?? ""), sessionId });
|
|
6852
|
-
return;
|
|
6853
|
-
// The orchestrator paused fan-out because a structural edit is queued; drain
|
|
6854
|
-
// it so the planner reconciles the plan and dependents resume spawning.
|
|
6855
|
-
// Dropped on the floor, the queue suppresses every later tick() — which is
|
|
6856
|
-
// exactly "tasks did not fan out when the dependent ones finished."
|
|
6857
|
-
case "queue_ready":
|
|
6858
|
-
dispatch({ type: "queueReady", sessionId });
|
|
6859
|
-
return;
|
|
6860
|
-
case "execution_complete":
|
|
6861
|
-
dispatch({ type: "executionComplete", summary: event.summary, sessionId });
|
|
6862
|
-
return;
|
|
6863
|
-
// A stop carries no tally — the reducer counts the pane instead.
|
|
6864
|
-
case "execution_stopped":
|
|
6865
|
-
dispatch({ type: "executionComplete", stopped: true, sessionId });
|
|
6866
|
-
return;
|
|
6867
|
-
// Nothing started; the user chooses how to go on.
|
|
6868
|
-
case "isolation_blocked":
|
|
6869
|
-
dispatch({ type: "isolationBlocked", message: event.message, ...event.repos ? { repos: event.repos } : {}, sessionId });
|
|
6870
|
-
return;
|
|
6871
|
-
// How the run isolates. The daemon has no toast channel, so this is the only place the user hears it.
|
|
6872
|
-
case "notice":
|
|
6873
|
-
dispatch({ type: "notice", message: event.message });
|
|
6874
|
-
return;
|
|
6875
|
-
case "isolation_handoff":
|
|
6876
|
-
dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
|
|
6877
|
-
return;
|
|
6878
|
-
// Raw runner chatter, and the planner's own turn: the status line and the
|
|
6879
|
-
// plan pane already say everything the user needs during a run. Listed
|
|
6880
|
-
// rather than defaulted, so a new SessionMessage variant fails to compile
|
|
6881
|
-
// here until someone decides what a run's watcher does with it.
|
|
6882
|
-
case "task_updated":
|
|
6883
|
-
case "task_output":
|
|
6884
|
-
case "plan_thinking":
|
|
6885
|
-
case "planner_liveness":
|
|
6886
|
-
case "research_step":
|
|
6887
|
-
case "plan_token":
|
|
6888
|
-
case "research_step_done":
|
|
6889
|
-
case "approval_request":
|
|
6890
|
-
case "approval_settled":
|
|
6891
|
-
return;
|
|
6892
|
-
// Its asker already has the words, from the merge request itself; a
|
|
6893
|
-
// viewer that did not ask still has to drop a run that is gone.
|
|
6894
|
-
case "isolation_merge":
|
|
6895
|
-
if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
|
|
6896
|
-
return;
|
|
6897
|
-
default: {
|
|
6898
|
-
const unhandled = event;
|
|
6899
|
-
void unhandled;
|
|
6900
|
-
return;
|
|
6901
|
-
}
|
|
6902
|
-
}
|
|
7304
|
+
function compactionSummary(plan) {
|
|
7305
|
+
const history = plan?.conversationHistory ?? [];
|
|
7306
|
+
const entry = history.find((message) => message.kind === "compaction");
|
|
7307
|
+
return entry ? { content: entry.content, ...entry.timestamp ? { timestamp: entry.timestamp } : {} } : null;
|
|
6903
7308
|
}
|
|
6904
7309
|
async function refreshPlan(deps, sessionId) {
|
|
6905
7310
|
const { plan } = await deps.api.getSession(sessionId, deps.workspace);
|
|
@@ -6946,6 +7351,7 @@ var CONTROL2 = {
|
|
|
6946
7351
|
"\v": "ctrl-k",
|
|
6947
7352
|
"\f": "ctrl-l",
|
|
6948
7353
|
"": "ctrl-n",
|
|
7354
|
+
"": "ctrl-o",
|
|
6949
7355
|
"": "ctrl-p",
|
|
6950
7356
|
"": "ctrl-u",
|
|
6951
7357
|
"": "ctrl-w"
|
|
@@ -7217,7 +7623,7 @@ async function handleTui(subArgs) {
|
|
|
7217
7623
|
}
|
|
7218
7624
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
7219
7625
|
try {
|
|
7220
|
-
(0,
|
|
7626
|
+
(0, import_core29.assertWorkspaceExists)(workspace);
|
|
7221
7627
|
} catch {
|
|
7222
7628
|
console.error(
|
|
7223
7629
|
`Workspace "${workspace}" does not exist.
|
|
@@ -7226,13 +7632,12 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
7226
7632
|
);
|
|
7227
7633
|
process.exit(1);
|
|
7228
7634
|
}
|
|
7229
|
-
registerSkillCommands((0,
|
|
7635
|
+
registerSkillCommands((0, import_core29.createSkillsService)(workspace).listSkills());
|
|
7230
7636
|
const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
|
|
7231
7637
|
const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
|
|
7232
7638
|
const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
|
|
7233
7639
|
let owned = ownedAtLaunch;
|
|
7234
7640
|
const api = new ApiClient(port, workspace);
|
|
7235
|
-
const conversationQueue = new ConversationQueue();
|
|
7236
7641
|
let terminal;
|
|
7237
7642
|
const autoMode = process.env.ORDEWELL_AUTONOMOUS_MODE;
|
|
7238
7643
|
const mouseCapture = process.env.ORDEWELL_TUI_MOUSE !== "false" && process.env.ORDEWELL_TUI_MOUSE !== "0";
|
|
@@ -7242,10 +7647,9 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
7242
7647
|
perform: (effect) => runEffect(effect, {
|
|
7243
7648
|
api,
|
|
7244
7649
|
workspace,
|
|
7245
|
-
conversationQueue,
|
|
7246
7650
|
port,
|
|
7247
7651
|
dispatch: (action) => app.dispatch(action),
|
|
7248
|
-
newSessionId: () => (0,
|
|
7652
|
+
newSessionId: () => (0, import_core29.mintSessionId)(),
|
|
7249
7653
|
setEnvVar: (key, value) => {
|
|
7250
7654
|
writeEnvVar(findEnvFile(), key, value);
|
|
7251
7655
|
process.env[key] = value;
|
|
@@ -7366,7 +7770,7 @@ function cliVersion() {
|
|
|
7366
7770
|
// src/main.ts
|
|
7367
7771
|
var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
|
|
7368
7772
|
async function main() {
|
|
7369
|
-
(0,
|
|
7773
|
+
(0, import_core30.migrateOldConfigDir)();
|
|
7370
7774
|
loadEnvFile();
|
|
7371
7775
|
const argv = process.argv.slice(2);
|
|
7372
7776
|
if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");
|