@ordewell/cli 0.4.23 → 0.5.1
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 +1600 -675
- 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_core21 = require("@ordewell/core");
|
|
28
28
|
|
|
29
29
|
// src/utils/env.ts
|
|
30
30
|
var import_fs = require("fs");
|
|
@@ -92,6 +92,9 @@ Planning:
|
|
|
92
92
|
ordewell stop Stop execution of the last session (or --server)
|
|
93
93
|
ordewell status List recent sessions (use --session-id for detail)
|
|
94
94
|
ordewell sessions list|load|delete Manage named sessions
|
|
95
|
+
ordewell fork Continue in a copy of the conversation and its tasks
|
|
96
|
+
ordewell compact Condense the conversation into a summary (tasks are kept)
|
|
97
|
+
ordewell rewind [<n>] Fork the conversation from before message <n> (no <n>: list them)
|
|
95
98
|
|
|
96
99
|
Tasks (<id> is an order number or a task ID):
|
|
97
100
|
ordewell add-task --title "..." Add a task to the current plan
|
|
@@ -598,6 +601,87 @@ var ApiClient = class {
|
|
|
598
601
|
}
|
|
599
602
|
return res.data;
|
|
600
603
|
}
|
|
604
|
+
/** Copy the conversation and its tasks into a new session the daemon has already adopted. */
|
|
605
|
+
async forkConversation(sessionId) {
|
|
606
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/fork`);
|
|
607
|
+
if (res.status !== 200) {
|
|
608
|
+
throw new Error(res.data?.error || "Fork failed");
|
|
609
|
+
}
|
|
610
|
+
return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan };
|
|
611
|
+
}
|
|
612
|
+
async rewindTargets(sessionId) {
|
|
613
|
+
const res = await this.httpRequest("GET", `/api/plans/${sessionId}/conversation/rewind-targets`);
|
|
614
|
+
if (res.status !== 200) {
|
|
615
|
+
throw new Error(res.data?.error || "Failed to list rewind targets");
|
|
616
|
+
}
|
|
617
|
+
return res.data.targets;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Fork the conversation from just before the user message at `index` (a
|
|
621
|
+
* transcript position) into a session the daemon has already adopted. The
|
|
622
|
+
* original is left as it was; `rewoundMessage` is that message in full.
|
|
623
|
+
*/
|
|
624
|
+
async rewindConversation(sessionId, index) {
|
|
625
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
|
|
626
|
+
if (res.status !== 200) {
|
|
627
|
+
throw new Error(res.data?.error || "Rewind failed");
|
|
628
|
+
}
|
|
629
|
+
return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan, rewoundMessage: res.data.rewoundMessage };
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Condense the conversation into a summary. One planner call, so it can take
|
|
633
|
+
* as long as a reply; a refusal or failure leaves the conversation as it was.
|
|
634
|
+
*/
|
|
635
|
+
async compactConversation(sessionId) {
|
|
636
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/compact`);
|
|
637
|
+
if (res.status !== 200) {
|
|
638
|
+
throw new Error(res.data?.error || "Compaction failed");
|
|
639
|
+
}
|
|
640
|
+
return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
|
|
641
|
+
}
|
|
642
|
+
async reviewRunDiff(sessionId) {
|
|
643
|
+
const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
|
|
644
|
+
if (res.status !== 200) {
|
|
645
|
+
throw new Error(res.data?.error || "Could not read the diff");
|
|
646
|
+
}
|
|
647
|
+
return res.data.diff;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* A conflict or a block is an outcome, not an error. Passed on whole: which
|
|
651
|
+
* repos blocked the merge, or landed before it stopped, is the answer.
|
|
652
|
+
*/
|
|
653
|
+
async mergeRun(sessionId) {
|
|
654
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/merge`);
|
|
655
|
+
if (res.status !== 200) {
|
|
656
|
+
throw new Error(res.data?.error || "Merge failed");
|
|
657
|
+
}
|
|
658
|
+
return res.data;
|
|
659
|
+
}
|
|
660
|
+
discardRun(sessionId) {
|
|
661
|
+
return this.isolationAction(sessionId, "discard", "Discard failed");
|
|
662
|
+
}
|
|
663
|
+
cleanupRun(sessionId) {
|
|
664
|
+
return this.isolationAction(sessionId, "cleanup", "Cleanup failed");
|
|
665
|
+
}
|
|
666
|
+
continueWithStash(sessionId) {
|
|
667
|
+
return this.isolationAction(sessionId, "stash-and-continue", "Could not stash and continue");
|
|
668
|
+
}
|
|
669
|
+
continueWithoutIsolation(sessionId) {
|
|
670
|
+
return this.isolationAction(sessionId, "run-without", "Could not continue without isolation");
|
|
671
|
+
}
|
|
672
|
+
async isolationAction(sessionId, segment, failure) {
|
|
673
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/${segment}`);
|
|
674
|
+
if (res.status !== 200) {
|
|
675
|
+
throw new Error(res.data?.error || failure);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async resolveConflictAsTask(sessionId, taskId) {
|
|
679
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`);
|
|
680
|
+
if (res.status !== 200) {
|
|
681
|
+
throw new Error(res.data?.error || "Could not add a resolver task");
|
|
682
|
+
}
|
|
683
|
+
return res.data.plan;
|
|
684
|
+
}
|
|
601
685
|
async closeSession(sessionId) {
|
|
602
686
|
const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/close`);
|
|
603
687
|
return res.data;
|
|
@@ -676,7 +760,7 @@ var ApiClient = class {
|
|
|
676
760
|
try {
|
|
677
761
|
const event = JSON.parse(data.toString());
|
|
678
762
|
onEvent(event);
|
|
679
|
-
if ((event.type === "execution_complete" || event.type === "execution_stopped") && !resolved) {
|
|
763
|
+
if ((event.type === "execution_complete" || event.type === "execution_stopped" || event.type === "isolation_blocked") && !resolved) {
|
|
680
764
|
resolved = true;
|
|
681
765
|
socket.close();
|
|
682
766
|
resolve3();
|
|
@@ -1176,6 +1260,113 @@ ${lastPlannerMessage(plan)}
|
|
|
1176
1260
|
}
|
|
1177
1261
|
}
|
|
1178
1262
|
|
|
1263
|
+
// src/isolation.ts
|
|
1264
|
+
var import_core7 = require("@ordewell/core");
|
|
1265
|
+
var STATE_OF = {
|
|
1266
|
+
active: "active",
|
|
1267
|
+
merged: "integrated",
|
|
1268
|
+
conflict: "conflict",
|
|
1269
|
+
repairing: "repairing",
|
|
1270
|
+
// A failed integration keeps its refs exactly as a failed verdict does; the
|
|
1271
|
+
// user sees one thing: work that did not land and can be looked at.
|
|
1272
|
+
kept: "kept",
|
|
1273
|
+
failed: "kept"
|
|
1274
|
+
};
|
|
1275
|
+
function reposOf(run) {
|
|
1276
|
+
if (Array.isArray(run.repos)) {
|
|
1277
|
+
const repos = run.repos.filter((r) => r && typeof r.path === "string" && typeof r.integrationBranch === "string" && typeof r.baseRef === "string");
|
|
1278
|
+
return repos.length > 0 ? repos.map((r) => ({ path: String(r.path), integrationBranch: String(r.integrationBranch), baseRef: String(r.baseRef) })) : null;
|
|
1279
|
+
}
|
|
1280
|
+
if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
|
|
1281
|
+
return [{ path: ".", integrationBranch: run.integrationBranch, baseRef: run.baseRef }];
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
function changedRepos(record, legacy, state) {
|
|
1286
|
+
if (legacy) return state === "integrated" ? ["."] : [];
|
|
1287
|
+
return Object.entries(record.repos ?? {}).filter(([, r]) => r?.changed === true).map(([path]) => path);
|
|
1288
|
+
}
|
|
1289
|
+
function isolationOfPlan(plan) {
|
|
1290
|
+
const run = plan?.isolation?.run;
|
|
1291
|
+
const repos = run ? reposOf(run) : null;
|
|
1292
|
+
if (!run || !repos) return null;
|
|
1293
|
+
const legacy = !Array.isArray(run.repos);
|
|
1294
|
+
const records = Object.entries(run.tasks ?? {});
|
|
1295
|
+
const tasks = {};
|
|
1296
|
+
const landed = [];
|
|
1297
|
+
for (const [taskId, record] of records) {
|
|
1298
|
+
const state = STATE_OF[String(record.status)];
|
|
1299
|
+
if (!state) continue;
|
|
1300
|
+
const changed = changedRepos(record, legacy, state);
|
|
1301
|
+
const repairedFiles = Array.isArray(record.repairedFiles) && record.repairedFiles.length > 0 ? record.repairedFiles.map(String) : void 0;
|
|
1302
|
+
tasks[taskId] = {
|
|
1303
|
+
state,
|
|
1304
|
+
branch: String(record.branch ?? ""),
|
|
1305
|
+
worktree: String(record.workspace ?? record.worktree ?? ""),
|
|
1306
|
+
repos: changed,
|
|
1307
|
+
...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {},
|
|
1308
|
+
...Array.isArray(record.conflictFiles) && record.conflictFiles.length > 0 ? { conflictFiles: record.conflictFiles.map(String) } : {},
|
|
1309
|
+
// No `repair` (attempt/limit): `conflictRepairAttempts` is server config, never
|
|
1310
|
+
// sent in a persisted plan, so it cannot be reconstructed here. A live
|
|
1311
|
+
// `tasksStatus` update fills it in once the stream catches up.
|
|
1312
|
+
...repairedFiles ? { repairedFiles } : {}
|
|
1313
|
+
};
|
|
1314
|
+
if (state === "integrated") {
|
|
1315
|
+
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed, ...repairedFiles ? { repairedFiles } : {} });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
landed.sort((a, b) => a.order - b.order);
|
|
1319
|
+
const entry = ({ taskId, order, title, repairedFiles }) => ({ taskId, order, title, ...repairedFiles ? { repairedFiles } : {} });
|
|
1320
|
+
return {
|
|
1321
|
+
handoff: {
|
|
1322
|
+
repos: repos.map((repo) => ({ ...repo, landed: landed.filter((t) => t.changed.includes(repo.path)).map(entry) })),
|
|
1323
|
+
landed: landed.map(entry)
|
|
1324
|
+
},
|
|
1325
|
+
tasks
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function handoffBranch(handoff) {
|
|
1329
|
+
return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
|
|
1330
|
+
}
|
|
1331
|
+
function handoffBase(handoff, length) {
|
|
1332
|
+
const group = isRepoGroup(handoff);
|
|
1333
|
+
return handoff.repos.map((r) => `${group ? `${r.path} ` : ""}${r.baseRef.slice(0, length)}`).join(", ");
|
|
1334
|
+
}
|
|
1335
|
+
function isRepoGroup(handoff) {
|
|
1336
|
+
return handoff.repos.some((r) => r.path !== ".");
|
|
1337
|
+
}
|
|
1338
|
+
function reposWithWork(handoff) {
|
|
1339
|
+
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
1340
|
+
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
1341
|
+
}
|
|
1342
|
+
function repairedLanded(handoff) {
|
|
1343
|
+
return handoff.landed.filter((t) => t.repairedFiles?.length);
|
|
1344
|
+
}
|
|
1345
|
+
function repairedNotice(handoff) {
|
|
1346
|
+
const repaired = repairedLanded(handoff);
|
|
1347
|
+
if (repaired.length === 0) return "";
|
|
1348
|
+
const named = repaired.map((t) => `${t.title} (${(0, import_core7.capConflictFiles)(t.repairedFiles)})`);
|
|
1349
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
1350
|
+
return ` ${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
1351
|
+
}
|
|
1352
|
+
function repoResultLines(handoff) {
|
|
1353
|
+
return handoff.repos.map(({ path, landed }) => {
|
|
1354
|
+
const n = landed.length;
|
|
1355
|
+
return `${path}: ${n === 0 ? "nothing to merge" : `${n} task${n === 1 ? "" : "s"} landed`}`;
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function taskRepoNames(isolation) {
|
|
1359
|
+
return (isolation?.repos ?? []).filter((r) => r !== ".");
|
|
1360
|
+
}
|
|
1361
|
+
function mergeOutcome(result, branch, group, repaired = []) {
|
|
1362
|
+
const ok = result.outcome === "merged";
|
|
1363
|
+
if (!group && result.outcome === "conflict") {
|
|
1364
|
+
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.` };
|
|
1365
|
+
}
|
|
1366
|
+
const { message } = (0, import_core7.describeMergeResult)(result, branch, group, repaired);
|
|
1367
|
+
return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1179
1370
|
// src/utils/output.ts
|
|
1180
1371
|
function iconFor(status) {
|
|
1181
1372
|
switch (status) {
|
|
@@ -1193,7 +1384,98 @@ function iconFor(status) {
|
|
|
1193
1384
|
}
|
|
1194
1385
|
|
|
1195
1386
|
// src/commands/run.ts
|
|
1196
|
-
var
|
|
1387
|
+
var import_core8 = require("@ordewell/core");
|
|
1388
|
+
|
|
1389
|
+
// src/catalog.ts
|
|
1390
|
+
function normalizeCatalog(result) {
|
|
1391
|
+
const runnersByModel = /* @__PURE__ */ new Map();
|
|
1392
|
+
for (const [runner, entries] of Object.entries(result.modelsByRunner ?? {})) {
|
|
1393
|
+
for (const entry of entries ?? []) {
|
|
1394
|
+
const id = String(entry.modelId ?? entry.id);
|
|
1395
|
+
runnersByModel.set(id, [.../* @__PURE__ */ new Set([...runnersByModel.get(id) ?? [], runner])]);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const models = (result.models ?? []).map((m) => {
|
|
1399
|
+
const id = String(m.modelId ?? m.id);
|
|
1400
|
+
return {
|
|
1401
|
+
id,
|
|
1402
|
+
label: String(m.modelLabel ?? m.label ?? id),
|
|
1403
|
+
provider: String(m.runnerProviderLabel ?? m.runnerProvider ?? m.provider ?? ""),
|
|
1404
|
+
pricing: m.pricing ? `$${m.pricing}/MTok` : void 0,
|
|
1405
|
+
variants: Array.isArray(m.variants) ? m.variants.map((v) => ({ id: String(v.id), label: String(v.label ?? v.id) })) : [],
|
|
1406
|
+
runners: runnersByModel.get(id) ?? []
|
|
1407
|
+
};
|
|
1408
|
+
});
|
|
1409
|
+
const orchestratorModels = (result.orchestratorModels ?? []).map((m) => ({
|
|
1410
|
+
id: String(m.id),
|
|
1411
|
+
label: String(m.label ?? m.id),
|
|
1412
|
+
provider: String(m.provider ?? ""),
|
|
1413
|
+
pricing: m.pricing ? `$${m.pricing}/MTok` : void 0
|
|
1414
|
+
}));
|
|
1415
|
+
return {
|
|
1416
|
+
models,
|
|
1417
|
+
orchestratorModels,
|
|
1418
|
+
providers: result.providers ?? [],
|
|
1419
|
+
providerErrors: result.providerErrors ?? {},
|
|
1420
|
+
modesByRunner: Object.fromEntries(
|
|
1421
|
+
Object.entries(result.modesByRunner ?? {}).map(([runner, modes]) => [
|
|
1422
|
+
runner,
|
|
1423
|
+
(modes ?? []).map((m) => ({
|
|
1424
|
+
id: String(m.id),
|
|
1425
|
+
label: String(m.label ?? m.id),
|
|
1426
|
+
description: m.description ? String(m.description) : void 0,
|
|
1427
|
+
autonomous: m.autonomous ? true : void 0
|
|
1428
|
+
}))
|
|
1429
|
+
])
|
|
1430
|
+
)
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/commands/shared.ts
|
|
1435
|
+
async function connect(subArgs, injected) {
|
|
1436
|
+
return injected ?? new ApiClient(await ensureDaemon(resolvePort(subArgs)));
|
|
1437
|
+
}
|
|
1438
|
+
async function fetchCatalog(api) {
|
|
1439
|
+
return normalizeCatalog(await api.getModels());
|
|
1440
|
+
}
|
|
1441
|
+
function fail(...lines) {
|
|
1442
|
+
for (const line of lines) console.error(line);
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
function writeEnv(env) {
|
|
1446
|
+
const envFile = findEnvFile();
|
|
1447
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1448
|
+
writeEnvVar(envFile, key, value);
|
|
1449
|
+
process.env[key] = value;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
async function persistEnv(api, env) {
|
|
1453
|
+
const settings = await api.updateSettings({ env });
|
|
1454
|
+
writeEnv(env);
|
|
1455
|
+
return settings;
|
|
1456
|
+
}
|
|
1457
|
+
function toTaskView(task) {
|
|
1458
|
+
return {
|
|
1459
|
+
id: task.id,
|
|
1460
|
+
order: task.order,
|
|
1461
|
+
title: task.title,
|
|
1462
|
+
description: task.description,
|
|
1463
|
+
prompt: task.prompt ?? void 0,
|
|
1464
|
+
type: task.type === "user" ? "user" : "ai",
|
|
1465
|
+
status: task.status ?? "pending",
|
|
1466
|
+
dependencies: task.dependencies ?? [],
|
|
1467
|
+
assignedRunner: task.assignedRunner || void 0,
|
|
1468
|
+
taskMode: task.taskMode || void 0,
|
|
1469
|
+
assignedModel: task.assignedModel ?? void 0,
|
|
1470
|
+
subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function taskViews(plan) {
|
|
1474
|
+
const tasks = allTasksOf(plan);
|
|
1475
|
+
return [...tasks].sort((a, b) => a.order - b.order).map(toTaskView);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// src/commands/run.ts
|
|
1197
1479
|
function resolveSessionId(subArgs) {
|
|
1198
1480
|
const explicit = flag(subArgs, "--session-id");
|
|
1199
1481
|
if (explicit) return explicit;
|
|
@@ -1206,7 +1488,9 @@ function resolveSessionId(subArgs) {
|
|
|
1206
1488
|
console.error(`Using last session: ${last.sessionId} ("${last.goal}")`);
|
|
1207
1489
|
return last.sessionId;
|
|
1208
1490
|
}
|
|
1209
|
-
async function followExecution(api, sessionId) {
|
|
1491
|
+
async function followExecution(api, sessionId, start, onBlocked) {
|
|
1492
|
+
let blocked = null;
|
|
1493
|
+
let dirtyRepos = [];
|
|
1210
1494
|
const taskStates = /* @__PURE__ */ new Map();
|
|
1211
1495
|
let lastPrinted = "";
|
|
1212
1496
|
function printStatus(tasks) {
|
|
@@ -1230,55 +1514,87 @@ async function followExecution(api, sessionId) {
|
|
|
1230
1514
|
lastPrinted = output;
|
|
1231
1515
|
}
|
|
1232
1516
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1517
|
+
let settleReady = () => {
|
|
1518
|
+
};
|
|
1519
|
+
const streamReady = new Promise((resolve3, reject) => {
|
|
1520
|
+
settleReady = (error) => error ? reject(error) : resolve3();
|
|
1521
|
+
});
|
|
1522
|
+
const stream = api.streamExecution(sessionId, (event) => {
|
|
1523
|
+
if (event.type === "task_started") {
|
|
1524
|
+
process.stderr.write(`[${event.order}/${event.title}] Started: ${event.runner} / ${event.modelId}
|
|
1237
1525
|
`);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1526
|
+
}
|
|
1527
|
+
if (event.type === "checkpoint") {
|
|
1528
|
+
process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core8.truncateCheckpointSummary)(event.summary)}
|
|
1241
1529
|
`);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1530
|
+
}
|
|
1531
|
+
if (event.type === "status_update" && event.tasks) {
|
|
1532
|
+
printStatus(event.tasks);
|
|
1533
|
+
}
|
|
1534
|
+
if (event.type === "review_needed") {
|
|
1535
|
+
console.log("\nPlan needs your sign-off \u2014 run `ordewell approve` to continue.");
|
|
1536
|
+
}
|
|
1537
|
+
if (event.type === "execution_complete") {
|
|
1538
|
+
const s = event;
|
|
1539
|
+
const summary = s.summary || { total: 0 };
|
|
1540
|
+
console.log(
|
|
1541
|
+
`
|
|
1254
1542
|
Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summary.blocked ?? summary.total - (summary.completed || 0) - (summary.failed || 0)} blocked.`
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
if (event.type === "execution_stopped") {
|
|
1546
|
+
console.log("\nExecution stopped.");
|
|
1547
|
+
}
|
|
1548
|
+
if (event.type === "notice") console.error(`\xB7 ${event.message}`);
|
|
1549
|
+
if (event.type === "isolation_blocked") {
|
|
1550
|
+
blocked = event.message;
|
|
1551
|
+
dirtyRepos = event.repos ?? [];
|
|
1552
|
+
}
|
|
1553
|
+
if (event.type === "isolation_handoff") {
|
|
1554
|
+
const n = event.landed.length;
|
|
1555
|
+
const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
|
|
1556
|
+
console.log(`
|
|
1557
|
+
Run finished on ${handoffBranch(event)}${where} \u2014 ${n} task${n === 1 ? "" : "s"} landed.`);
|
|
1558
|
+
if (isRepoGroup(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
|
|
1559
|
+
console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
|
|
1560
|
+
}
|
|
1561
|
+
}, settleReady);
|
|
1562
|
+
const streamFailed = (err) => {
|
|
1262
1563
|
console.error(`Execution stream error: ${err.message}`);
|
|
1263
1564
|
process.exit(1);
|
|
1565
|
+
};
|
|
1566
|
+
void stream.catch(settleReady);
|
|
1567
|
+
await streamReady.catch(streamFailed);
|
|
1568
|
+
await start();
|
|
1569
|
+
await stream.catch(streamFailed);
|
|
1570
|
+
if (blocked === null) return;
|
|
1571
|
+
if (!onBlocked) {
|
|
1572
|
+
await api.stopExecution(sessionId);
|
|
1573
|
+
console.error(blocked);
|
|
1574
|
+
const where = dirtyRepos.length > 0 ? ` in ${dirtyRepos.join(", ")}` : "";
|
|
1575
|
+
console.error(`Nothing was started. Re-run with \`--stash\` to stash your tracked changes${where} first, or \`--without-isolation\` to run in your working tree this once.`);
|
|
1576
|
+
process.exit(1);
|
|
1264
1577
|
}
|
|
1578
|
+
await followExecution(api, sessionId, () => onBlocked === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
|
|
1265
1579
|
}
|
|
1266
|
-
async function handleRun(subArgs) {
|
|
1580
|
+
async function handleRun(subArgs, injectedApi) {
|
|
1267
1581
|
const sessionId = resolveSessionId(subArgs);
|
|
1268
|
-
const api =
|
|
1582
|
+
const api = await connect(subArgs, injectedApi);
|
|
1269
1583
|
console.error(`Executing plan...`);
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1584
|
+
const onBlocked = hasFlag(subArgs, "--stash") ? "stash" : hasFlag(subArgs, "--without-isolation") ? "shared" : void 0;
|
|
1585
|
+
await followExecution(api, sessionId, async () => {
|
|
1586
|
+
try {
|
|
1587
|
+
await api.executePlan(sessionId);
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
console.error(`Failed to start execution: ${err.message}`);
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
}, onBlocked);
|
|
1277
1593
|
}
|
|
1278
1594
|
|
|
1279
1595
|
// src/commands/status.ts
|
|
1280
1596
|
var import_fs4 = require("fs");
|
|
1281
|
-
var
|
|
1597
|
+
var import_core9 = require("@ordewell/core");
|
|
1282
1598
|
async function handleStatus(subArgs, injectedApi) {
|
|
1283
1599
|
const sessionId = flag(subArgs, "--session-id");
|
|
1284
1600
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
@@ -1349,12 +1665,12 @@ function printOneSession(sessionId, meta, plan) {
|
|
|
1349
1665
|
const icon = iconFor(t.status);
|
|
1350
1666
|
const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
|
|
1351
1667
|
const shortId = (t.id ?? "").slice(-8);
|
|
1352
|
-
console.log(` ${(0,
|
|
1668
|
+
console.log(` ${(0, import_core9.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
|
|
1353
1669
|
for (const sub of t.subtasks || []) {
|
|
1354
1670
|
const subIcon = iconFor(sub.status);
|
|
1355
1671
|
const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
|
|
1356
1672
|
const subShortId = (sub.id ?? "").slice(-8);
|
|
1357
|
-
console.log(` ${(0,
|
|
1673
|
+
console.log(` ${(0, import_core9.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
|
|
1358
1674
|
}
|
|
1359
1675
|
}
|
|
1360
1676
|
}
|
|
@@ -1406,123 +1722,32 @@ async function handleWeb(subArgs) {
|
|
|
1406
1722
|
}
|
|
1407
1723
|
|
|
1408
1724
|
// src/commands/models.ts
|
|
1409
|
-
var
|
|
1725
|
+
var import_core11 = require("@ordewell/core");
|
|
1410
1726
|
|
|
1411
1727
|
// src/commands/model.ts
|
|
1412
|
-
var
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
id: String(m.id),
|
|
1436
|
-
label: String(m.label ?? m.id),
|
|
1437
|
-
provider: String(m.provider ?? ""),
|
|
1438
|
-
pricing: m.pricing ? `$${m.pricing}/MTok` : void 0
|
|
1439
|
-
}));
|
|
1440
|
-
return {
|
|
1441
|
-
models,
|
|
1442
|
-
orchestratorModels,
|
|
1443
|
-
providers: result.providers ?? [],
|
|
1444
|
-
providerErrors: result.providerErrors ?? {},
|
|
1445
|
-
modesByRunner: Object.fromEntries(
|
|
1446
|
-
Object.entries(result.modesByRunner ?? {}).map(([runner, modes]) => [
|
|
1447
|
-
runner,
|
|
1448
|
-
(modes ?? []).map((m) => ({
|
|
1449
|
-
id: String(m.id),
|
|
1450
|
-
label: String(m.label ?? m.id),
|
|
1451
|
-
description: m.description ? String(m.description) : void 0,
|
|
1452
|
-
autonomous: m.autonomous ? true : void 0
|
|
1453
|
-
}))
|
|
1454
|
-
])
|
|
1455
|
-
)
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// src/commands/shared.ts
|
|
1460
|
-
async function connect(subArgs, injected) {
|
|
1461
|
-
return injected ?? new ApiClient(await ensureDaemon(resolvePort(subArgs)));
|
|
1462
|
-
}
|
|
1463
|
-
async function fetchCatalog(api) {
|
|
1464
|
-
return normalizeCatalog(await api.getModels());
|
|
1465
|
-
}
|
|
1466
|
-
function fail(...lines) {
|
|
1467
|
-
for (const line of lines) console.error(line);
|
|
1468
|
-
process.exit(1);
|
|
1469
|
-
}
|
|
1470
|
-
function writeEnv(env) {
|
|
1471
|
-
const envFile = findEnvFile();
|
|
1472
|
-
for (const [key, value] of Object.entries(env)) {
|
|
1473
|
-
writeEnvVar(envFile, key, value);
|
|
1474
|
-
process.env[key] = value;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
async function persistEnv(api, env) {
|
|
1478
|
-
const settings = await api.updateSettings({ env });
|
|
1479
|
-
writeEnv(env);
|
|
1480
|
-
return settings;
|
|
1481
|
-
}
|
|
1482
|
-
function toTaskView(task) {
|
|
1483
|
-
return {
|
|
1484
|
-
id: task.id,
|
|
1485
|
-
order: task.order,
|
|
1486
|
-
title: task.title,
|
|
1487
|
-
description: task.description,
|
|
1488
|
-
prompt: task.prompt ?? void 0,
|
|
1489
|
-
type: task.type === "user" ? "user" : "ai",
|
|
1490
|
-
status: task.status ?? "pending",
|
|
1491
|
-
dependencies: task.dependencies ?? [],
|
|
1492
|
-
assignedRunner: task.assignedRunner || void 0,
|
|
1493
|
-
taskMode: task.taskMode || void 0,
|
|
1494
|
-
assignedModel: task.assignedModel ?? void 0,
|
|
1495
|
-
subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
function taskViews(plan) {
|
|
1499
|
-
const tasks = allTasksOf(plan);
|
|
1500
|
-
return [...tasks].sort((a, b) => a.order - b.order).map(toTaskView);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// src/commands/model.ts
|
|
1504
|
-
var USAGE = "Usage: ordewell model [show | set <model-id>]";
|
|
1505
|
-
async function handleModel(subArgs, injectedApi) {
|
|
1506
|
-
const [action, modelId] = positionals(subArgs);
|
|
1507
|
-
if (action && action !== "show" && action !== "set") {
|
|
1508
|
-
fail(`Unknown model action: "${action}"`, USAGE);
|
|
1509
|
-
}
|
|
1510
|
-
const api = await connect(subArgs, injectedApi);
|
|
1511
|
-
if (action !== "set") {
|
|
1512
|
-
await showModels(api);
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
if (!modelId) fail(USAGE);
|
|
1516
|
-
const settings = await api.getSettings();
|
|
1517
|
-
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1518
|
-
const runner = (0, import_core9.runnerForProvider)(provider);
|
|
1519
|
-
if (runner) {
|
|
1520
|
-
const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
|
|
1521
|
-
if (known.length > 0 && !known.some((m) => m.id === modelId)) {
|
|
1522
|
-
fail(
|
|
1523
|
-
`${modelId} was not discovered for ${runner}.`,
|
|
1524
|
-
`Run \`ordewell model\` to see what ${runner} offers.`
|
|
1525
|
-
);
|
|
1728
|
+
var import_core10 = require("@ordewell/core");
|
|
1729
|
+
var USAGE = "Usage: ordewell model [show | set <model-id>]";
|
|
1730
|
+
async function handleModel(subArgs, injectedApi) {
|
|
1731
|
+
const [action, modelId] = positionals(subArgs);
|
|
1732
|
+
if (action && action !== "show" && action !== "set") {
|
|
1733
|
+
fail(`Unknown model action: "${action}"`, USAGE);
|
|
1734
|
+
}
|
|
1735
|
+
const api = await connect(subArgs, injectedApi);
|
|
1736
|
+
if (action !== "set") {
|
|
1737
|
+
await showModels(api);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
if (!modelId) fail(USAGE);
|
|
1741
|
+
const settings = await api.getSettings();
|
|
1742
|
+
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1743
|
+
const runner = (0, import_core10.runnerForProvider)(provider);
|
|
1744
|
+
if (runner) {
|
|
1745
|
+
const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
|
|
1746
|
+
if (known.length > 0 && !known.some((m) => m.id === modelId)) {
|
|
1747
|
+
fail(
|
|
1748
|
+
`${modelId} was not discovered for ${runner}.`,
|
|
1749
|
+
`Run \`ordewell model\` to see what ${runner} offers.`
|
|
1750
|
+
);
|
|
1526
1751
|
}
|
|
1527
1752
|
}
|
|
1528
1753
|
await api.updateSettings({ orchestratorModel: modelId });
|
|
@@ -1534,7 +1759,7 @@ async function showModels(api) {
|
|
|
1534
1759
|
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
1535
1760
|
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1536
1761
|
const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
1537
|
-
const runner = (0,
|
|
1762
|
+
const runner = (0, import_core10.runnerForProvider)(provider);
|
|
1538
1763
|
const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
|
|
1539
1764
|
console.log(`
|
|
1540
1765
|
Planner model: ${current || "(backend default)"}`);
|
|
@@ -1568,8 +1793,8 @@ async function handleModels(subArgs) {
|
|
|
1568
1793
|
const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
|
|
1569
1794
|
return handleModel(["set", subArgs[setIdx + 1], ...rest]);
|
|
1570
1795
|
}
|
|
1571
|
-
const config = new
|
|
1572
|
-
const providers = (0,
|
|
1796
|
+
const config = new import_core11.EnvConfig();
|
|
1797
|
+
const providers = (0, import_core11.configuredProviders)(config);
|
|
1573
1798
|
if (providers.length === 0) {
|
|
1574
1799
|
console.error("No providers configured. Set an API key for a provider, e.g.:");
|
|
1575
1800
|
console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
|
|
@@ -1578,19 +1803,19 @@ async function handleModels(subArgs) {
|
|
|
1578
1803
|
console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
|
|
1579
1804
|
process.exit(1);
|
|
1580
1805
|
}
|
|
1581
|
-
const { apiKeys, baseUrls } = (0,
|
|
1806
|
+
const { apiKeys, baseUrls } = (0, import_core11.collectProviderCredentials)(config);
|
|
1582
1807
|
let models;
|
|
1583
1808
|
let errors;
|
|
1584
1809
|
try {
|
|
1585
|
-
const result = await (0,
|
|
1586
|
-
models = (0,
|
|
1810
|
+
const result = await (0, import_core11.fetchAllProviderModels)({ apiKeys, baseUrls });
|
|
1811
|
+
models = (0, import_core11.toOrchestratorOptions)(result.models, import_core11.ORCHESTRATOR_SHORTCUTS);
|
|
1587
1812
|
errors = result.errors;
|
|
1588
1813
|
} catch (err) {
|
|
1589
1814
|
console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
|
|
1590
1815
|
process.exit(1);
|
|
1591
1816
|
return;
|
|
1592
1817
|
}
|
|
1593
|
-
const providerLabel = (p) => (0,
|
|
1818
|
+
const providerLabel = (p) => (0, import_core11.getProviderMeta)(p)?.label ?? p;
|
|
1594
1819
|
console.log(
|
|
1595
1820
|
`
|
|
1596
1821
|
Available models (${models.length}) across ${providers.length} configured provider(s):`
|
|
@@ -1707,9 +1932,9 @@ function handleSetup(subArgs) {
|
|
|
1707
1932
|
}
|
|
1708
1933
|
|
|
1709
1934
|
// src/commands/plugins.ts
|
|
1710
|
-
var
|
|
1935
|
+
var import_core12 = require("@ordewell/core");
|
|
1711
1936
|
function handlePlugins(subArgs) {
|
|
1712
|
-
const registry = new
|
|
1937
|
+
const registry = new import_core12.RunnerRegistry();
|
|
1713
1938
|
registry.loadUserPlugins();
|
|
1714
1939
|
const sub = subArgs[0];
|
|
1715
1940
|
if (sub === "list" || !sub) {
|
|
@@ -1745,7 +1970,7 @@ ${plugins.length} plugin(s):
|
|
|
1745
1970
|
process.exit(1);
|
|
1746
1971
|
}
|
|
1747
1972
|
try {
|
|
1748
|
-
const resolved = (0,
|
|
1973
|
+
const resolved = (0, import_core12.classifyPluginSource)(source);
|
|
1749
1974
|
const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
|
|
1750
1975
|
console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
|
|
1751
1976
|
} catch (err) {
|
|
@@ -2139,153 +2364,627 @@ async function handleSessions(subArgs, injectedApi) {
|
|
|
2139
2364
|
process.exit(1);
|
|
2140
2365
|
}
|
|
2141
2366
|
|
|
2142
|
-
// src/commands/
|
|
2143
|
-
var
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const model = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
2148
|
-
const effort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
|
|
2149
|
-
if (!model) return { model: "", effort: "", source: "none" };
|
|
2150
|
-
return rememberedModelFor(settings, provider) === model ? { model, effort, source: "remembered" } : { model, effort, source: "catalog-default" };
|
|
2151
|
-
}
|
|
2152
|
-
function rememberedModelFor(settings, provider) {
|
|
2153
|
-
const map = settings.plannerModels;
|
|
2154
|
-
if (!map || typeof map !== "object") return void 0;
|
|
2155
|
-
const entry = map[provider];
|
|
2156
|
-
if (!entry || typeof entry !== "object") return void 0;
|
|
2157
|
-
const { model } = entry;
|
|
2158
|
-
return typeof model === "string" ? model : void 0;
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
// src/commands/planner.ts
|
|
2162
|
-
var KNOWN_PROVIDERS = Object.keys(import_core12.ALL_PROVIDERS);
|
|
2163
|
-
async function handlePlanner(subArgs, injectedApi) {
|
|
2367
|
+
// src/commands/conversation.ts
|
|
2368
|
+
var REWIND_USAGE = "Usage: ordewell rewind [<n>] [--session-id <id>] [--workspace /path] \u2014 with no <n>, lists the messages you can rewind to.";
|
|
2369
|
+
async function adopted(subArgs, injectedApi) {
|
|
2370
|
+
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
2371
|
+
const sessionId = resolveSessionId(subArgs);
|
|
2164
2372
|
const api = await connect(subArgs, injectedApi);
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
await
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
const provider = wanted.toLowerCase();
|
|
2171
|
-
if (!KNOWN_PROVIDERS.includes(provider)) {
|
|
2172
|
-
fail(`Unknown planner: ${provider}`, "Run `ordewell planner` with no arguments to see the list.");
|
|
2173
|
-
}
|
|
2174
|
-
const settings = await persistEnv(api, { AI_PROVIDER: provider });
|
|
2175
|
-
const recall2 = describePlannerSwitch(settings, provider);
|
|
2176
|
-
writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
|
|
2177
|
-
const meta = import_core12.ALL_PROVIDERS[provider];
|
|
2178
|
-
const catalog = await fetchCatalog(api);
|
|
2179
|
-
console.log(
|
|
2180
|
-
(0, import_core12.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
|
|
2181
|
-
);
|
|
2182
|
-
console.log(plannerModelLine(recall2));
|
|
2183
|
-
if (!(0, import_core12.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
|
|
2184
|
-
console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
|
|
2373
|
+
let plan;
|
|
2374
|
+
try {
|
|
2375
|
+
({ plan } = await api.adoptSession(sessionId, workspace));
|
|
2376
|
+
} catch (err) {
|
|
2377
|
+
fail(`Failed to load session: ${err.message}`);
|
|
2185
2378
|
}
|
|
2379
|
+
return { api, sessionId, workspace, plan };
|
|
2186
2380
|
}
|
|
2187
|
-
function
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
return " Pick a model with `ordewell model set <id>`, or leave it on the default.";
|
|
2381
|
+
async function handleFork(subArgs, injectedApi) {
|
|
2382
|
+
const { api, sessionId, workspace } = await adopted(subArgs, injectedApi);
|
|
2383
|
+
let fork;
|
|
2384
|
+
try {
|
|
2385
|
+
fork = await api.forkConversation(sessionId);
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
fail(`Failed to fork: ${err.message}`);
|
|
2195
2388
|
}
|
|
2389
|
+
saveLastSession(fork.sessionId, fork.goal, fork.plan.runners ?? [], workspace);
|
|
2390
|
+
console.log(`Forked ${sessionId} into ${fork.sessionId} \u2014 the fork is now the current session.`);
|
|
2391
|
+
console.log(` \`ordewell sessions load ${sessionId}\` to go back to the original.`);
|
|
2196
2392
|
}
|
|
2197
|
-
async function
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core12.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
|
|
2393
|
+
async function handleCompact(subArgs, injectedApi) {
|
|
2394
|
+
const { api, sessionId } = await adopted(subArgs, injectedApi);
|
|
2395
|
+
let result;
|
|
2396
|
+
try {
|
|
2397
|
+
result = await api.compactConversation(sessionId);
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
fail(`Failed to condense: ${err.message}`);
|
|
2205
2400
|
}
|
|
2206
|
-
console.log(
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2401
|
+
console.log(`Condensed ${sessionId}. The last two exchanges were kept as they were; the tasks are unchanged.
|
|
2402
|
+
`);
|
|
2403
|
+
console.log(result.summary);
|
|
2404
|
+
}
|
|
2405
|
+
async function handleRewind(subArgs, injectedApi) {
|
|
2406
|
+
const arg = positionals(subArgs)[0];
|
|
2407
|
+
if (arg !== void 0 && !/^\d+$/.test(arg)) fail(REWIND_USAGE);
|
|
2408
|
+
const { api, sessionId, workspace } = await adopted(subArgs, injectedApi);
|
|
2409
|
+
if (arg === void 0) {
|
|
2410
|
+
let targets;
|
|
2411
|
+
try {
|
|
2412
|
+
targets = await api.rewindTargets(sessionId);
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
fail(`Failed to list messages: ${err.message}`);
|
|
2415
|
+
}
|
|
2416
|
+
if (targets.length === 0) {
|
|
2417
|
+
console.log("Nothing to rewind to yet \u2014 the only message so far is the goal.");
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
console.log("Your messages, most recent first:\n");
|
|
2421
|
+
for (const t of [...targets].reverse()) console.log(` ${String(t.index).padStart(4)} ${t.preview}`);
|
|
2422
|
+
console.log("\n `ordewell rewind <n>` forks the conversation from just before message <n>. The original is kept, and the tasks ride along.");
|
|
2423
|
+
return;
|
|
2211
2424
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2425
|
+
let fork;
|
|
2426
|
+
try {
|
|
2427
|
+
fork = await api.rewindConversation(sessionId, Number(arg));
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
fail(`Failed to rewind: ${err.message}`);
|
|
2430
|
+
}
|
|
2431
|
+
saveLastSession(fork.sessionId, fork.goal, fork.plan.runners ?? [], workspace);
|
|
2432
|
+
console.log(`Forked ${sessionId} into ${fork.sessionId} from just before message ${arg} \u2014 the fork is now the current session.`);
|
|
2433
|
+
console.log(` \`ordewell sessions load ${sessionId}\` to go back to the original.`);
|
|
2434
|
+
console.log("\nThe message you rewound to, to resend or edit:\n");
|
|
2435
|
+
console.log(fork.rewoundMessage);
|
|
2214
2436
|
}
|
|
2215
|
-
|
|
2216
|
-
|
|
2437
|
+
|
|
2438
|
+
// src/commands/handoff.ts
|
|
2439
|
+
var import_readline2 = require("readline");
|
|
2440
|
+
|
|
2441
|
+
// src/tui/ansi.ts
|
|
2442
|
+
var import_string_width = __toESM(require("string-width"));
|
|
2443
|
+
var ESCAPE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\|$)|[P^_][\s\S]*?(?:\x1b\\|$)|\[[0-?]*[ -/]*[@-~]?|[ -/]*[0-~]?)/g;
|
|
2444
|
+
var ESCAPE_SPLIT = new RegExp(`(${ESCAPE.source})`);
|
|
2445
|
+
var CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
2446
|
+
var CONTROL_IN_ROW = /[\x00-\x1f\x7f-\x9f]/g;
|
|
2447
|
+
var HAS_CONTROL = /[\x00-\x1f\x7f-\x9f]/;
|
|
2448
|
+
var PAINT_ESCAPE = /^\x1b\[[0-9;]*[mGK]$/;
|
|
2449
|
+
function stripAnsi(text) {
|
|
2450
|
+
return text.replace(ESCAPE, "");
|
|
2217
2451
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2452
|
+
function sanitize(text) {
|
|
2453
|
+
return text.replace(ESCAPE, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(CONTROL, "");
|
|
2454
|
+
}
|
|
2455
|
+
function paintOnly(line) {
|
|
2456
|
+
if (!HAS_CONTROL.test(line)) return line;
|
|
2457
|
+
return line.split(ESCAPE_SPLIT).map((piece) => piece.startsWith("\x1B") ? PAINT_ESCAPE.test(piece) ? piece : "" : piece.replace(CONTROL_IN_ROW, "")).join("");
|
|
2458
|
+
}
|
|
2459
|
+
function width(text) {
|
|
2460
|
+
return (0, import_string_width.default)(stripAnsi(text));
|
|
2461
|
+
}
|
|
2462
|
+
function truncate(text, max) {
|
|
2463
|
+
if (max <= 0) return "";
|
|
2464
|
+
if (width(text) <= max) return text;
|
|
2465
|
+
let out = "";
|
|
2466
|
+
let used = 0;
|
|
2467
|
+
for (const char of stripAnsi(text)) {
|
|
2468
|
+
const w = (0, import_string_width.default)(char);
|
|
2469
|
+
if (used + w > max - 1) break;
|
|
2470
|
+
out += char;
|
|
2471
|
+
used += w;
|
|
2232
2472
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2473
|
+
return out + "\u2026";
|
|
2474
|
+
}
|
|
2475
|
+
function pad(text, target) {
|
|
2476
|
+
const current = width(text);
|
|
2477
|
+
if (current > target) return truncate(text, target);
|
|
2478
|
+
return text + " ".repeat(target - current);
|
|
2479
|
+
}
|
|
2480
|
+
function chopWord(piece, max) {
|
|
2481
|
+
const chunks = [];
|
|
2482
|
+
let cur = "";
|
|
2483
|
+
let curWidth = 0;
|
|
2484
|
+
for (const ch of piece) {
|
|
2485
|
+
const w = (0, import_string_width.default)(ch);
|
|
2486
|
+
if (cur && curWidth + w > max) {
|
|
2487
|
+
chunks.push(cur);
|
|
2488
|
+
cur = ch;
|
|
2489
|
+
curWidth = w;
|
|
2241
2490
|
} else {
|
|
2242
|
-
|
|
2491
|
+
cur += ch;
|
|
2492
|
+
curWidth += w;
|
|
2243
2493
|
}
|
|
2244
|
-
console.log(` ${EFFORT_USAGE}`);
|
|
2245
|
-
return;
|
|
2246
2494
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2495
|
+
chunks.push(cur);
|
|
2496
|
+
return chunks;
|
|
2497
|
+
}
|
|
2498
|
+
var wrapMemo = /* @__PURE__ */ new Map();
|
|
2499
|
+
var WRAP_MEMO_MAX = 64;
|
|
2500
|
+
function wrapLines(text, max) {
|
|
2501
|
+
const key = `${max}\0${text}`;
|
|
2502
|
+
const cached = wrapMemo.get(key);
|
|
2503
|
+
if (cached) {
|
|
2504
|
+
wrapMemo.delete(key);
|
|
2505
|
+
wrapMemo.set(key, cached);
|
|
2506
|
+
return cached;
|
|
2252
2507
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2508
|
+
const result = computeWrapLines(text, max);
|
|
2509
|
+
if (wrapMemo.size >= WRAP_MEMO_MAX) {
|
|
2510
|
+
const oldest = wrapMemo.keys().next().value;
|
|
2511
|
+
if (oldest !== void 0) wrapMemo.delete(oldest);
|
|
2255
2512
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2513
|
+
wrapMemo.set(key, result);
|
|
2514
|
+
return result;
|
|
2515
|
+
}
|
|
2516
|
+
function computeWrapLines(text, max) {
|
|
2517
|
+
if (max <= 0) return [{ line: "", start: 0 }];
|
|
2518
|
+
const out = [];
|
|
2519
|
+
let pos = 0;
|
|
2520
|
+
for (const paragraph of text.split("\n")) {
|
|
2521
|
+
let current = "";
|
|
2522
|
+
let currentStart = pos;
|
|
2523
|
+
let first = true;
|
|
2524
|
+
let wordPos = pos;
|
|
2525
|
+
for (const word of paragraph.split(" ")) {
|
|
2526
|
+
let piece = word;
|
|
2527
|
+
let pieceStart = wordPos;
|
|
2528
|
+
if (width(piece) > max) {
|
|
2529
|
+
const chunks = chopWord(piece, max);
|
|
2530
|
+
for (let ci = 0; ci < chunks.length - 1; ci++) {
|
|
2531
|
+
const head = chunks[ci];
|
|
2532
|
+
if (!first) {
|
|
2533
|
+
out.push({ line: current, start: currentStart });
|
|
2534
|
+
current = "";
|
|
2535
|
+
}
|
|
2536
|
+
out.push({ line: head, start: pieceStart });
|
|
2537
|
+
pieceStart += head.length;
|
|
2538
|
+
first = true;
|
|
2539
|
+
}
|
|
2540
|
+
piece = chunks[chunks.length - 1];
|
|
2541
|
+
}
|
|
2542
|
+
const candidate = first ? piece : `${current} ${piece}`;
|
|
2543
|
+
const candidateStart = first ? pieceStart : currentStart;
|
|
2544
|
+
first = false;
|
|
2545
|
+
if (width(candidate) > max) {
|
|
2546
|
+
out.push({ line: current, start: currentStart });
|
|
2547
|
+
current = piece;
|
|
2548
|
+
currentStart = pieceStart;
|
|
2549
|
+
} else {
|
|
2550
|
+
current = candidate;
|
|
2551
|
+
currentStart = candidateStart;
|
|
2552
|
+
}
|
|
2553
|
+
wordPos += word.length + 1;
|
|
2554
|
+
}
|
|
2555
|
+
out.push({ line: current, start: currentStart });
|
|
2556
|
+
pos += paragraph.length + 1;
|
|
2557
|
+
}
|
|
2558
|
+
return out.length > 0 ? out : [{ line: "", start: 0 }];
|
|
2559
|
+
}
|
|
2560
|
+
function wrap(text, max) {
|
|
2561
|
+
return wrapLines(text, max).map((l) => l.line);
|
|
2562
|
+
}
|
|
2563
|
+
function colour(open) {
|
|
2564
|
+
return (text) => style.enabled ? `\x1B[${open}m${text}\x1B[0m` : text;
|
|
2565
|
+
}
|
|
2566
|
+
var style = {
|
|
2567
|
+
/** Set false when the output is piped or the terminal has no colour. */
|
|
2568
|
+
enabled: true,
|
|
2569
|
+
bold: colour("1"),
|
|
2570
|
+
dim: colour("2"),
|
|
2571
|
+
italic: colour("3"),
|
|
2572
|
+
underline: colour("4"),
|
|
2573
|
+
inverse: colour("7"),
|
|
2574
|
+
red: colour("31"),
|
|
2575
|
+
green: colour("32"),
|
|
2576
|
+
yellow: colour("33"),
|
|
2577
|
+
blue: colour("34"),
|
|
2578
|
+
magenta: colour("35"),
|
|
2579
|
+
cyan: colour("36"),
|
|
2580
|
+
grey: colour("90")
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
// src/tui/transcript.ts
|
|
2584
|
+
function say(state, role, content, research, extra) {
|
|
2585
|
+
const message = {
|
|
2586
|
+
role,
|
|
2587
|
+
content: sanitize(content),
|
|
2588
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2589
|
+
...research ? { research } : {},
|
|
2590
|
+
...extra
|
|
2591
|
+
};
|
|
2592
|
+
return { ...state, messages: [...state.messages, message], scroll: 0 };
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// src/tui/handoff.ts
|
|
2596
|
+
var HANDOFF_ACTIONS = [
|
|
2597
|
+
{ id: "review", label: "Review diff", hint: "the branch against the commit it forked from" },
|
|
2598
|
+
{ id: "merge", label: "Merge", hint: "into the branch you have checked out \u2014 asks first" },
|
|
2599
|
+
{ id: "discard", label: "Discard", hint: "the run, its worktrees and this branch \u2014 asks first" },
|
|
2600
|
+
{ id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
|
|
2601
|
+
];
|
|
2602
|
+
function handoffActions(handoff) {
|
|
2603
|
+
if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
|
|
2604
|
+
return [
|
|
2605
|
+
{ id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
|
|
2606
|
+
{ id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there \u2014 asks first" },
|
|
2607
|
+
{ id: "discard", label: "Discard", hint: "the run, its worktrees and every repository's branch \u2014 asks first" },
|
|
2608
|
+
{ id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches everywhere, keep the integration branches" }
|
|
2609
|
+
];
|
|
2610
|
+
}
|
|
2611
|
+
var reposOf2 = (handoff) => handoff.repos.map((r) => r.path).join(", ");
|
|
2612
|
+
var HANDOFF_USAGE = `/handoff [${HANDOFF_ACTIONS.map((a) => a.id).join("|")}]`;
|
|
2613
|
+
var diffRoom = (rows) => Math.max(1, rows - 3);
|
|
2614
|
+
var fail2 = (state, content) => ({ state: say(state, "error", content), effects: [] });
|
|
2615
|
+
function handoffCommand(state, arg) {
|
|
2616
|
+
if (!state.sessionId || !state.handoff) return fail2(state, "No isolated run to hand off \u2014 a plan that ran in worktrees leaves one here.");
|
|
2617
|
+
if (arg === void 0) return { state: { ...state, overlay: { kind: "handoff", index: 0, diff: null } }, effects: [] };
|
|
2618
|
+
const action = HANDOFF_ACTIONS.find((a) => a.id === arg.toLowerCase());
|
|
2619
|
+
if (!action) return fail2(state, `Usage: ${HANDOFF_USAGE}`);
|
|
2620
|
+
return runHandoffAction(state, action.id);
|
|
2621
|
+
}
|
|
2622
|
+
function handoffArrived(state, handoff) {
|
|
2623
|
+
const landed = handoff.landed.length;
|
|
2624
|
+
const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
|
|
2625
|
+
const told = say(
|
|
2626
|
+
{ ...state, handoff },
|
|
2627
|
+
"system",
|
|
2628
|
+
`Run finished on ${handoffBranch(handoff)}${where} \u2014 ${landed} task${landed === 1 ? "" : "s"} landed. /handoff to review and land it.`
|
|
2629
|
+
);
|
|
2630
|
+
return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
|
|
2631
|
+
}
|
|
2632
|
+
function runHandoffAction(state, id) {
|
|
2633
|
+
const { sessionId, handoff } = state;
|
|
2634
|
+
if (!sessionId || !handoff) return fail2(state, "No isolated run to hand off.");
|
|
2635
|
+
const closed = { ...state, overlay: null };
|
|
2636
|
+
switch (id) {
|
|
2637
|
+
case "review":
|
|
2638
|
+
return { state, effects: [{ type: "isolationReviewDiff", sessionId }] };
|
|
2639
|
+
case "cleanup":
|
|
2640
|
+
return { state: closed, effects: [{ type: "isolationCleanup", sessionId, branch: handoffBranch(handoff) }] };
|
|
2641
|
+
case "merge":
|
|
2642
|
+
return {
|
|
2643
|
+
state: {
|
|
2644
|
+
...state,
|
|
2645
|
+
overlay: isRepoGroup(handoff) ? {
|
|
2646
|
+
kind: "confirm",
|
|
2647
|
+
title: "Merge all into your branches?",
|
|
2648
|
+
message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. Ordewell never does this on its own. It checks every repository first and merges none unless every repository can take it; on git older than 2.38 it goes repository by repository and stops at the first that fails. A merge that conflicts is aborted, and your trees stay as they were. Once every repository has merged, the run's worktrees and branches are removed.${repairedNotice(handoff)}`,
|
|
2649
|
+
action: { kind: "merge-run" }
|
|
2650
|
+
} : {
|
|
2651
|
+
kind: "confirm",
|
|
2652
|
+
title: "Merge into your branch?",
|
|
2653
|
+
message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. Ordewell never does this on its own. If it conflicts the merge is aborted and your tree stays as it was. Once it has merged, the run's worktrees and branches are removed.${repairedNotice(handoff)}`,
|
|
2654
|
+
action: { kind: "merge-run" }
|
|
2655
|
+
}
|
|
2656
|
+
},
|
|
2657
|
+
effects: []
|
|
2658
|
+
};
|
|
2659
|
+
case "discard":
|
|
2660
|
+
return {
|
|
2661
|
+
state: {
|
|
2662
|
+
...state,
|
|
2663
|
+
overlay: {
|
|
2664
|
+
kind: "confirm",
|
|
2665
|
+
title: "Discard this run?",
|
|
2666
|
+
message: `Removes the run's worktrees and task branches and deletes ${handoffBranch(handoff)}${isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : ""}. Tasks keep their status \u2014 mark one not done if you did not keep its work.`,
|
|
2667
|
+
action: { kind: "discard-run" }
|
|
2668
|
+
}
|
|
2669
|
+
},
|
|
2670
|
+
effects: []
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function confirmedHandoff(state, kind) {
|
|
2675
|
+
const closed = { ...state, overlay: null };
|
|
2676
|
+
if (!state.sessionId || !state.handoff) return { state: closed, effects: [] };
|
|
2677
|
+
const { sessionId, handoff } = state;
|
|
2678
|
+
const repaired = repairedLanded(handoff);
|
|
2679
|
+
const effect = kind === "merge-run" ? {
|
|
2680
|
+
type: "isolationMerge",
|
|
2681
|
+
sessionId,
|
|
2682
|
+
branch: handoffBranch(handoff),
|
|
2683
|
+
...isRepoGroup(handoff) ? { group: true } : {},
|
|
2684
|
+
...repaired.length > 0 ? { repaired } : {}
|
|
2685
|
+
} : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
|
|
2686
|
+
return { state: closed, effects: [effect] };
|
|
2687
|
+
}
|
|
2688
|
+
function showDiff(state, diff) {
|
|
2689
|
+
if (diff.trim() === "") return say(state, "system", `Nothing differs from ${state.handoff ? handoffBase(state.handoff, 8) : "the base commit"}.`);
|
|
2690
|
+
const index = state.overlay?.kind === "handoff" ? state.overlay.index : 0;
|
|
2691
|
+
const lines = sanitize(diff.replace(/\t/g, " ")).split("\n");
|
|
2692
|
+
return { ...state, overlay: { kind: "handoff", index, diff: { lines, scroll: 0 } } };
|
|
2693
|
+
}
|
|
2694
|
+
function handleHandoffKey(state, overlay, key, rows) {
|
|
2695
|
+
const hold = { state, effects: [] };
|
|
2696
|
+
const up = key.name === "up" || key.name === "scrollup";
|
|
2697
|
+
const down = key.name === "down" || key.name === "scrolldown";
|
|
2698
|
+
if (overlay.diff) {
|
|
2699
|
+
const { lines, scroll } = overlay.diff;
|
|
2700
|
+
const room = diffRoom(rows);
|
|
2701
|
+
const max = Math.max(0, lines.length - room);
|
|
2702
|
+
const delta = up ? -1 : down ? 1 : key.name === "pageup" ? -room : key.name === "pagedown" ? room : null;
|
|
2703
|
+
if (delta !== null) {
|
|
2704
|
+
const next = Math.min(max, Math.max(0, scroll + delta));
|
|
2705
|
+
return { state: { ...state, overlay: { ...overlay, diff: { lines, scroll: next } } }, effects: [] };
|
|
2706
|
+
}
|
|
2707
|
+
if (key.name === "escape" || key.name === "enter") return { state: { ...state, overlay: { ...overlay, diff: null } }, effects: [] };
|
|
2708
|
+
return hold;
|
|
2709
|
+
}
|
|
2710
|
+
if (key.name === "escape") return { state: { ...state, overlay: null }, effects: [] };
|
|
2711
|
+
if (up || down) {
|
|
2712
|
+
const index = Math.min(HANDOFF_ACTIONS.length - 1, Math.max(0, overlay.index + (up ? -1 : 1)));
|
|
2713
|
+
return { state: { ...state, overlay: { ...overlay, index } }, effects: [] };
|
|
2714
|
+
}
|
|
2715
|
+
if (key.name === "enter") return runHandoffAction(state, HANDOFF_ACTIONS[overlay.index].id);
|
|
2716
|
+
return hold;
|
|
2717
|
+
}
|
|
2718
|
+
var BLOCKED_ITEMS = [
|
|
2719
|
+
{ id: "stash", label: "Stash and continue", detail: "git stash your tracked changes, then run in worktrees" },
|
|
2720
|
+
{ id: "shared", label: "Run without isolation", detail: "this run only, in your working tree" },
|
|
2721
|
+
{ id: "cancel", label: "Cancel", detail: "start nothing" }
|
|
2722
|
+
];
|
|
2723
|
+
function blockedPicker(state, message, repos = []) {
|
|
2724
|
+
const items = repos.length === 0 ? BLOCKED_ITEMS : BLOCKED_ITEMS.map((item) => item.id === "stash" ? { ...item, detail: `git stash the tracked changes in ${repos.join(", ")}, then run in worktrees` } : item);
|
|
2725
|
+
return {
|
|
2726
|
+
...state,
|
|
2727
|
+
overlay: {
|
|
2728
|
+
kind: "picker",
|
|
2729
|
+
picker: {
|
|
2730
|
+
title: "Run blocked by uncommitted changes",
|
|
2731
|
+
hint: message,
|
|
2732
|
+
items,
|
|
2733
|
+
filter: "",
|
|
2734
|
+
index: 0,
|
|
2735
|
+
multi: false,
|
|
2736
|
+
chosen: [],
|
|
2737
|
+
action: { kind: "isolation-blocked" }
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function chooseBlocked(state, choice) {
|
|
2743
|
+
const closed = { ...state, overlay: null };
|
|
2744
|
+
if (!state.sessionId) return { state: closed, effects: [] };
|
|
2745
|
+
const { sessionId } = state;
|
|
2746
|
+
if (choice === "cancel") return { state: closed, effects: [{ type: "stopExecution", sessionId }] };
|
|
2747
|
+
return { state: closed, effects: [{ type: "isolationContinue", sessionId, mode: choice }] };
|
|
2748
|
+
}
|
|
2749
|
+
function sameIsolation(a, b) {
|
|
2750
|
+
return a?.state === b?.state && a?.branch === b?.branch && a?.worktree === b?.worktree && a?.conflictRepo === b?.conflictRepo && (a?.repos ?? []).join("\0") === (b?.repos ?? []).join("\0") && (a?.conflictFiles ?? []).join("\0") === (b?.conflictFiles ?? []).join("\0") && a?.repair?.attempt === b?.repair?.attempt && a?.repair?.limit === b?.repair?.limit && (a?.repairedFiles ?? []).join("\0") === (b?.repairedFiles ?? []).join("\0");
|
|
2751
|
+
}
|
|
2752
|
+
function mapTasks(tasks, map) {
|
|
2753
|
+
return tasks.map((task) => {
|
|
2754
|
+
const mapped = map(task);
|
|
2755
|
+
return task.subtasks ? { ...mapped, subtasks: mapTasks(task.subtasks, map) } : mapped;
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
function clearIsolation(state) {
|
|
2759
|
+
return {
|
|
2760
|
+
...state,
|
|
2761
|
+
handoff: null,
|
|
2762
|
+
tasks: mapTasks(state.tasks, ({ isolation: _dropped, ...task }) => task)
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
function isolationForPlan(tasks, record, previous) {
|
|
2766
|
+
const known = /* @__PURE__ */ new Map();
|
|
2767
|
+
mapTasks(previous, (task) => {
|
|
2768
|
+
if (task.isolation) known.set(task.id, task.isolation);
|
|
2769
|
+
return task;
|
|
2770
|
+
});
|
|
2771
|
+
return mapTasks(tasks, (task) => {
|
|
2772
|
+
const isolation = record ? record.tasks[task.id] ?? { state: "none" } : known.get(task.id);
|
|
2773
|
+
return isolation ? { ...task, isolation } : task;
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// src/commands/handoff.ts
|
|
2778
|
+
var USAGE4 = "Usage: ordewell handoff [review|merge|discard|cleanup] [--session-id <id>] [--workspace /path] [--yes]";
|
|
2779
|
+
async function askYesNo(question) {
|
|
2780
|
+
if (!process.stdin.isTTY) return false;
|
|
2781
|
+
const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
|
|
2782
|
+
try {
|
|
2783
|
+
const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
|
|
2784
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
2785
|
+
} finally {
|
|
2786
|
+
rl.close();
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
2790
|
+
const action = positionals(subArgs)[0]?.toLowerCase();
|
|
2791
|
+
if (action === void 0) {
|
|
2792
|
+
console.log(`${USAGE4}
|
|
2793
|
+
`);
|
|
2794
|
+
for (const a of HANDOFF_ACTIONS) console.log(` ${a.id.padEnd(8)} ${a.label} \u2014 ${a.hint}`);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
if (!HANDOFF_ACTIONS.some((a) => a.id === action)) fail(`Unknown handoff action "${action}".`, USAGE4);
|
|
2798
|
+
const { api, sessionId, plan } = await adopted(subArgs, injectedApi);
|
|
2799
|
+
const handoff = isolationOfPlan(plan)?.handoff;
|
|
2800
|
+
if (!handoff) fail("This session has no isolated run to hand off.");
|
|
2801
|
+
const branch = handoffBranch(handoff);
|
|
2802
|
+
const group = isRepoGroup(handoff);
|
|
2803
|
+
if (group) for (const line of repoResultLines(handoff)) console.error(line);
|
|
2804
|
+
const asked = hasFlag(subArgs, "--yes");
|
|
2805
|
+
const confirmed = async (question) => {
|
|
2806
|
+
if (asked || await confirm(question)) return;
|
|
2807
|
+
fail("Not confirmed \u2014 nothing was changed. Pass --yes to confirm without a prompt.");
|
|
2808
|
+
};
|
|
2809
|
+
const attempt = async (work) => {
|
|
2810
|
+
try {
|
|
2811
|
+
return await work();
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
return fail(`Handoff ${action} failed: ${err.message}`);
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
switch (action) {
|
|
2817
|
+
case "review": {
|
|
2818
|
+
const diff = await attempt(() => api.reviewRunDiff(sessionId));
|
|
2819
|
+
console.log(diff.trim() === "" ? `Nothing differs from ${handoffBase(handoff, 8)}.` : diff);
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
case "merge": {
|
|
2823
|
+
await confirmed((group ? `Merge ${branch} into the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it; once all have, the run's worktrees and branches are removed.` : `Merge ${branch} into the branch you have checked out? Once merged, the run's worktrees and branches are removed.`) + repairedNotice(handoff));
|
|
2824
|
+
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
|
|
2825
|
+
if (ok) console.log(message);
|
|
2826
|
+
else fail(message);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
case "discard":
|
|
2830
|
+
await confirmed(group ? `Discard this run's worktrees and task branches, and delete ${branch} in every repository?` : `Discard this run's worktrees and task branches, and delete ${branch}?`);
|
|
2831
|
+
await attempt(() => api.discardRun(sessionId));
|
|
2832
|
+
console.log(`Discarded the run and ${branch}${group ? " in every repository" : ""}.`);
|
|
2833
|
+
return;
|
|
2834
|
+
case "cleanup":
|
|
2835
|
+
await attempt(() => api.cleanupRun(sessionId));
|
|
2836
|
+
console.log(`Removed the run's worktrees and task branches; ${branch} is kept${group ? " in every repository" : ""}.`);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
// src/commands/planner.ts
|
|
2842
|
+
var import_core13 = require("@ordewell/core");
|
|
2843
|
+
|
|
2844
|
+
// src/plannerModelSwitch.ts
|
|
2845
|
+
function describePlannerSwitch(settings, provider) {
|
|
2846
|
+
const model = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
2847
|
+
const effort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
|
|
2848
|
+
if (!model) return { model: "", effort: "", source: "none" };
|
|
2849
|
+
return rememberedModelFor(settings, provider) === model ? { model, effort, source: "remembered" } : { model, effort, source: "catalog-default" };
|
|
2850
|
+
}
|
|
2851
|
+
function rememberedModelFor(settings, provider) {
|
|
2852
|
+
const map = settings.plannerModels;
|
|
2853
|
+
if (!map || typeof map !== "object") return void 0;
|
|
2854
|
+
const entry = map[provider];
|
|
2855
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
2856
|
+
const { model } = entry;
|
|
2857
|
+
return typeof model === "string" ? model : void 0;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
// src/commands/planner.ts
|
|
2861
|
+
var KNOWN_PROVIDERS = Object.keys(import_core13.ALL_PROVIDERS);
|
|
2862
|
+
async function handlePlanner(subArgs, injectedApi) {
|
|
2863
|
+
const api = await connect(subArgs, injectedApi);
|
|
2864
|
+
const [wanted] = positionals(subArgs);
|
|
2865
|
+
if (!wanted || wanted === "show") {
|
|
2866
|
+
await showPlanners(api);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
const provider = wanted.toLowerCase();
|
|
2870
|
+
if (!KNOWN_PROVIDERS.includes(provider)) {
|
|
2871
|
+
fail(`Unknown planner: ${provider}`, "Run `ordewell planner` with no arguments to see the list.");
|
|
2872
|
+
}
|
|
2873
|
+
const settings = await persistEnv(api, { AI_PROVIDER: provider });
|
|
2874
|
+
const recall2 = describePlannerSwitch(settings, provider);
|
|
2875
|
+
writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
|
|
2876
|
+
const meta = import_core13.ALL_PROVIDERS[provider];
|
|
2877
|
+
const catalog = await fetchCatalog(api);
|
|
2878
|
+
console.log(
|
|
2879
|
+
(0, import_core13.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
|
|
2880
|
+
);
|
|
2881
|
+
console.log(plannerModelLine(recall2));
|
|
2882
|
+
if (!(0, import_core13.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
|
|
2883
|
+
console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
function plannerModelLine(recall2) {
|
|
2887
|
+
switch (recall2.source) {
|
|
2888
|
+
case "remembered":
|
|
2889
|
+
return ` Restored its planner model: ${recall2.model}.`;
|
|
2890
|
+
case "catalog-default":
|
|
2891
|
+
return ` Using ${recall2.model}, its default model \u2014 pick another with \`ordewell model set <id>\`.`;
|
|
2892
|
+
case "none":
|
|
2893
|
+
return " Pick a model with `ordewell model set <id>`, or leave it on the default.";
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
async function showPlanners(api) {
|
|
2897
|
+
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
2898
|
+
const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
2899
|
+
console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
|
|
2900
|
+
for (const id of import_core13.CLI_PROVIDERS) {
|
|
2901
|
+
const runner = (0, import_core13.runnerForProvider)(id);
|
|
2902
|
+
const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
|
|
2903
|
+
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core13.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
|
|
2904
|
+
}
|
|
2905
|
+
console.log("\nAPI providers");
|
|
2906
|
+
for (const id of import_core13.PROVIDER_PRIORITY) {
|
|
2907
|
+
const configured = catalog.providers.includes(id);
|
|
2908
|
+
const meta = import_core13.ALL_PROVIDERS[id];
|
|
2909
|
+
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
|
|
2910
|
+
}
|
|
2911
|
+
console.log(`
|
|
2912
|
+
* = current. Set with: ordewell planner <id>`);
|
|
2913
|
+
}
|
|
2914
|
+
function marker(selected) {
|
|
2915
|
+
return selected ? "*" : " ";
|
|
2916
|
+
}
|
|
2917
|
+
var EFFORT_USAGE = "Usage: ordewell planner-effort [<level>|default]";
|
|
2918
|
+
async function handlePlannerEffort(subArgs, injectedApi) {
|
|
2919
|
+
const api = await connect(subArgs, injectedApi);
|
|
2920
|
+
const [wanted] = positionals(subArgs);
|
|
2921
|
+
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
2922
|
+
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
2923
|
+
const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
2924
|
+
const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
|
|
2925
|
+
const runner = (0, import_core13.runnerForProvider)(provider);
|
|
2926
|
+
if (!runner) {
|
|
2927
|
+
fail(
|
|
2928
|
+
"Thinking effort applies to a coding-agent planner.",
|
|
2929
|
+
"Switch with: ordewell planner claude-code|codex|opencode"
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
const model = catalog.models.find((m) => m.id === modelId && m.runners?.includes(runner));
|
|
2933
|
+
const variants = model?.variants ?? [];
|
|
2934
|
+
if (!wanted || wanted === "show") {
|
|
2935
|
+
console.log(`Planner effort: ${currentEffort || "runner default"}`);
|
|
2936
|
+
if (!model) {
|
|
2937
|
+
console.log(` No planner model selected for ${runner} \u2014 pick one with \`ordewell model set <id>\`.`);
|
|
2938
|
+
} else if (variants.length === 0) {
|
|
2939
|
+
console.log(` ${model.label} exposes no effort levels; it always runs at the agent's default.`);
|
|
2940
|
+
} else {
|
|
2941
|
+
console.log(` Available for ${model.label}: ${variants.map((v) => v.id).join(", ")}, default`);
|
|
2942
|
+
}
|
|
2943
|
+
console.log(` ${EFFORT_USAGE}`);
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
const level = wanted.toLowerCase();
|
|
2947
|
+
if (level === "default") {
|
|
2948
|
+
await persistEnv(api, { ORDEWELL_PLANNER_EFFORT: "" });
|
|
2949
|
+
console.log("Planner effort set to the runner default.");
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (!model) {
|
|
2953
|
+
fail(`No planner model selected for ${runner}.`, "Pick one with: ordewell model set <id>");
|
|
2954
|
+
}
|
|
2955
|
+
if (!variants.some((v) => v.id === level)) {
|
|
2956
|
+
fail(
|
|
2957
|
+
variants.length > 0 ? `Unknown effort: ${wanted}. Available for ${model.label}: ${variants.map((v) => v.id).join(", ")}, default.` : `${model.label} exposes no effort levels; it always runs at the agent's default.`
|
|
2958
|
+
);
|
|
2260
2959
|
}
|
|
2261
2960
|
await persistEnv(api, { ORDEWELL_PLANNER_EFFORT: level });
|
|
2262
2961
|
console.log(`Planner effort set to ${level}.`);
|
|
2263
2962
|
}
|
|
2264
2963
|
|
|
2265
2964
|
// src/commands/key.ts
|
|
2266
|
-
var
|
|
2267
|
-
var
|
|
2965
|
+
var import_core14 = require("@ordewell/core");
|
|
2966
|
+
var USAGE5 = "Usage: ordewell key [list | set <provider> <key>]";
|
|
2268
2967
|
async function handleKey(subArgs, injectedApi) {
|
|
2269
2968
|
const [action, provider, ...keyParts] = positionals(subArgs);
|
|
2270
2969
|
if (action && action !== "list" && action !== "set") {
|
|
2271
|
-
fail(`Unknown key action: "${action}"`,
|
|
2970
|
+
fail(`Unknown key action: "${action}"`, USAGE5);
|
|
2272
2971
|
}
|
|
2273
2972
|
const api = await connect(subArgs, injectedApi);
|
|
2274
2973
|
if (action !== "set") {
|
|
2275
2974
|
const catalog = await fetchCatalog(api);
|
|
2276
2975
|
console.log("\nAPI provider keys\n");
|
|
2277
|
-
for (const id2 of
|
|
2278
|
-
const meta2 =
|
|
2976
|
+
for (const id2 of import_core14.PROVIDER_PRIORITY) {
|
|
2977
|
+
const meta2 = import_core14.ALL_PROVIDERS[id2];
|
|
2279
2978
|
const configured = catalog.providers.includes(id2);
|
|
2280
2979
|
console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
|
|
2281
2980
|
}
|
|
2282
2981
|
console.log(`
|
|
2283
|
-
\u2713 = a working key was found. ${
|
|
2982
|
+
\u2713 = a working key was found. ${USAGE5}`);
|
|
2284
2983
|
return;
|
|
2285
2984
|
}
|
|
2286
|
-
if (!provider) fail(
|
|
2985
|
+
if (!provider) fail(USAGE5);
|
|
2287
2986
|
const id = provider.toLowerCase();
|
|
2288
|
-
const meta =
|
|
2987
|
+
const meta = import_core14.ALL_PROVIDERS[id];
|
|
2289
2988
|
if (!meta) {
|
|
2290
2989
|
fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
|
|
2291
2990
|
}
|
|
@@ -2296,7 +2995,7 @@ async function handleKey(subArgs, injectedApi) {
|
|
|
2296
2995
|
}
|
|
2297
2996
|
|
|
2298
2997
|
// src/commands/runners.ts
|
|
2299
|
-
var
|
|
2998
|
+
var USAGE6 = "Usage: ordewell runners [<runner-id> on|off]";
|
|
2300
2999
|
async function handleRunners(subArgs, injectedApi) {
|
|
2301
3000
|
const [runner, toggle] = positionals(subArgs);
|
|
2302
3001
|
const api = await connect(subArgs, injectedApi);
|
|
@@ -2307,7 +3006,7 @@ async function handleRunners(subArgs, injectedApi) {
|
|
|
2307
3006
|
console.log(` ${r.enabled ? "\u2713" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (disabled)"}`);
|
|
2308
3007
|
}
|
|
2309
3008
|
console.log(`
|
|
2310
|
-
\u2713 = offered to the planner. ${
|
|
3009
|
+
\u2713 = offered to the planner. ${USAGE6}`);
|
|
2311
3010
|
return;
|
|
2312
3011
|
}
|
|
2313
3012
|
const state = await api.getRunners();
|
|
@@ -2316,7 +3015,7 @@ async function handleRunners(subArgs, injectedApi) {
|
|
|
2316
3015
|
fail(`Unknown runner: ${runner}`, `Enabled or not, this daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
|
|
2317
3016
|
}
|
|
2318
3017
|
const enabled = toggle === void 0 ? !known.enabled : toggle === "on" ? true : toggle === "off" ? false : null;
|
|
2319
|
-
if (enabled === null) fail(
|
|
3018
|
+
if (enabled === null) fail(USAGE6);
|
|
2320
3019
|
await api.setRunnerEnabled(runner, enabled);
|
|
2321
3020
|
console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
|
|
2322
3021
|
}
|
|
@@ -2352,20 +3051,21 @@ async function handleRefresh(subArgs, injectedApi) {
|
|
|
2352
3051
|
async function handleApprove(subArgs, injectedApi) {
|
|
2353
3052
|
const sessionId = resolveSessionId(subArgs);
|
|
2354
3053
|
const api = await connect(subArgs, injectedApi);
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
3054
|
+
await followExecution(api, sessionId, async () => {
|
|
3055
|
+
try {
|
|
3056
|
+
await api.approveReview(sessionId);
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
console.error(`Failed to approve: ${err.message}`);
|
|
3059
|
+
process.exit(1);
|
|
3060
|
+
}
|
|
3061
|
+
console.error("Plan approved \u2014 continuing execution.");
|
|
3062
|
+
});
|
|
2363
3063
|
}
|
|
2364
3064
|
|
|
2365
3065
|
// src/tui/terminalLauncher.ts
|
|
2366
3066
|
var import_child_process3 = require("child_process");
|
|
2367
3067
|
var import_util = require("util");
|
|
2368
|
-
var
|
|
3068
|
+
var import_core15 = require("@ordewell/core");
|
|
2369
3069
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
2370
3070
|
var defaultExecFile = async (command, args) => {
|
|
2371
3071
|
const { stdout, stderr } = await execFileAsync(command, args);
|
|
@@ -2385,16 +3085,16 @@ var defaultWhich = (bin) => {
|
|
|
2385
3085
|
async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
|
|
2386
3086
|
const platform = deps.platform ?? process.platform;
|
|
2387
3087
|
const env = deps.env ?? process.env;
|
|
2388
|
-
const hasTmuxImpl = deps.hasTmuxImpl ??
|
|
3088
|
+
const hasTmuxImpl = deps.hasTmuxImpl ?? import_core15.hasTmux;
|
|
2389
3089
|
const execFileImpl = deps.execFileImpl ?? defaultExecFile;
|
|
2390
3090
|
const spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
2391
3091
|
const which = deps.which ?? defaultWhich;
|
|
2392
3092
|
if (!hasTmuxImpl()) {
|
|
2393
3093
|
return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
|
|
2394
3094
|
}
|
|
2395
|
-
const session = (0,
|
|
2396
|
-
const socket = (0,
|
|
2397
|
-
const base = (0,
|
|
3095
|
+
const session = (0, import_core15.tmuxSessionName)(port);
|
|
3096
|
+
const socket = (0, import_core15.tmuxSocketName)(port);
|
|
3097
|
+
const base = (0, import_core15.tmuxWindowName)(taskId, planSessionId);
|
|
2398
3098
|
let windows;
|
|
2399
3099
|
try {
|
|
2400
3100
|
const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
|
|
@@ -2598,239 +3298,97 @@ Model \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assigne
|
|
|
2598
3298
|
var EFFORT_USAGE2 = "Usage: ordewell task-effort <task-id-or-order> [<level>|default] [--session-id <id>]";
|
|
2599
3299
|
async function handleTaskEffort(subArgs, injectedApi) {
|
|
2600
3300
|
await withTask(subArgs, EFFORT_USAGE2, injectedApi, async (api, sessionId, task, _tasks, level) => {
|
|
2601
|
-
const catalog = await fetchCatalog(api);
|
|
2602
|
-
const variants = effortsForTask(catalog.models, task);
|
|
2603
|
-
const current = task.assignedModel?.thinkingEffort;
|
|
2604
|
-
if (!level) {
|
|
2605
|
-
console.log(`
|
|
2606
|
-
Thinking effort \xB7 #${task.order} ${task.title}
|
|
2607
|
-
`);
|
|
2608
|
-
console.log(` ${current ? " " : "*"} default Let the executor choose`);
|
|
2609
|
-
for (const v of variants) {
|
|
2610
|
-
console.log(` ${v.id === current ? "*" : " "} ${v.id.padEnd(14)} ${v.label}`);
|
|
2611
|
-
}
|
|
2612
|
-
if (variants.length === 0) {
|
|
2613
|
-
console.log(`
|
|
2614
|
-
${task.assignedModel?.modelLabel ?? "This model"} exposes no effort levels.`);
|
|
2615
|
-
}
|
|
2616
|
-
console.log(`
|
|
2617
|
-
${EFFORT_USAGE2}`);
|
|
2618
|
-
return;
|
|
2619
|
-
}
|
|
2620
|
-
const wanted = level.toLowerCase();
|
|
2621
|
-
const thinkingEffort = wanted === "default" ? void 0 : wanted;
|
|
2622
|
-
if (thinkingEffort && !variants.some((v) => v.id === thinkingEffort)) {
|
|
2623
|
-
fail(
|
|
2624
|
-
variants.length > 0 ? `Unknown effort: ${level}. Available: ${variants.map((v) => v.id).join(", ")}, default.` : `${task.assignedModel?.modelLabel ?? "This task model"} exposes no effort levels.`
|
|
2625
|
-
);
|
|
2626
|
-
}
|
|
2627
|
-
await api.updateTask(sessionId, task.id, {
|
|
2628
|
-
assignedModel: task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0,
|
|
2629
|
-
thinkingEffort: thinkingEffort ?? null
|
|
2630
|
-
});
|
|
2631
|
-
console.log(`Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`);
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
var MODE_USAGE = "Usage: ordewell task-mode <task-id-or-order> [<mode>] [--session-id <id>]";
|
|
2635
|
-
async function handleTaskMode(subArgs, injectedApi) {
|
|
2636
|
-
await withTask(subArgs, MODE_USAGE, injectedApi, async (api, sessionId, task, _tasks, mode) => {
|
|
2637
|
-
const catalog = await fetchCatalog(api);
|
|
2638
|
-
const modes = modesForTask(catalog.modesByRunner, task);
|
|
2639
|
-
if (!mode) {
|
|
2640
|
-
console.log(`
|
|
2641
|
-
Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
|
|
2642
|
-
`);
|
|
2643
|
-
if (modes.length === 0) {
|
|
2644
|
-
console.log(" This task has no runner, or its runner declares no modes.");
|
|
2645
|
-
}
|
|
2646
|
-
for (const m of modes) {
|
|
2647
|
-
console.log(` ${m.id === task.taskMode ? "*" : " "} ${m.id.padEnd(14)} ${m.label}${m.description ? ` \u2014 ${m.description}` : ""}`);
|
|
2648
|
-
}
|
|
2649
|
-
console.log(`
|
|
2650
|
-
${MODE_USAGE}`);
|
|
2651
|
-
return;
|
|
2652
|
-
}
|
|
2653
|
-
const chosen = modes.find((m) => m.id === mode);
|
|
2654
|
-
if (!chosen) {
|
|
2655
|
-
fail(
|
|
2656
|
-
modes.length > 0 ? `Unknown mode "${mode}" for ${task.assignedRunner}. Available: ${modes.map((m) => m.id).join(", ")}.` : `${task.assignedRunner ?? "This task"} declares no modes.`
|
|
2657
|
-
);
|
|
2658
|
-
}
|
|
2659
|
-
await api.updateTask(sessionId, task.id, { taskMode: chosen.id });
|
|
2660
|
-
console.log(`Task #${task.order} mode set to ${chosen.label}.`);
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
|
|
2664
|
-
async function handleTaskDeps(subArgs, injectedApi) {
|
|
2665
|
-
await withTask(subArgs, DEPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
|
|
2666
|
-
const candidates = (0, import_plan_utils2.dependencyCandidates)(tasks, task.id);
|
|
2667
|
-
if (!value) {
|
|
2668
|
-
console.log(`
|
|
2669
|
-
Dependencies \xB7 #${task.order} ${task.title}
|
|
2670
|
-
`);
|
|
2671
|
-
if (candidates.length === 0) {
|
|
2672
|
-
console.log(` Nothing runs before #${task.order}, so it has no possible dependencies.`);
|
|
2673
|
-
}
|
|
2674
|
-
for (const c of candidates) {
|
|
2675
|
-
const chosen = task.dependencies.includes(c.id);
|
|
2676
|
-
console.log(` ${chosen ? "*" : " "} ${c.id} #${c.order} ${c.title}${c.status === "completed" ? " (already completed)" : ""}`);
|
|
2677
|
-
}
|
|
2678
|
-
console.log(`
|
|
2679
|
-
* = current. ${DEPS_USAGE}`);
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
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);
|
|
2683
|
-
const check = (0, import_plan_utils2.canSetDependencies)(tasks, task.id, dependencies);
|
|
2684
|
-
if (!check.ok) fail(check.error ?? "Invalid dependencies.");
|
|
2685
|
-
await api.updateTask(sessionId, task.id, { dependencies });
|
|
2686
|
-
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.`);
|
|
2687
|
-
});
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
// src/tui/index.ts
|
|
2691
|
-
var import_core18 = require("@ordewell/core");
|
|
2692
|
-
|
|
2693
|
-
// src/tui/ansi.ts
|
|
2694
|
-
var import_string_width = __toESM(require("string-width"));
|
|
2695
|
-
var ESCAPE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\|$)|[P^_][\s\S]*?(?:\x1b\\|$)|\[[0-?]*[ -/]*[@-~]?|[ -/]*[0-~]?)/g;
|
|
2696
|
-
var ESCAPE_SPLIT = new RegExp(`(${ESCAPE.source})`);
|
|
2697
|
-
var CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
2698
|
-
var CONTROL_IN_ROW = /[\x00-\x1f\x7f-\x9f]/g;
|
|
2699
|
-
var HAS_CONTROL = /[\x00-\x1f\x7f-\x9f]/;
|
|
2700
|
-
var PAINT_ESCAPE = /^\x1b\[[0-9;]*[mGK]$/;
|
|
2701
|
-
function stripAnsi(text) {
|
|
2702
|
-
return text.replace(ESCAPE, "");
|
|
2703
|
-
}
|
|
2704
|
-
function sanitize(text) {
|
|
2705
|
-
return text.replace(ESCAPE, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(CONTROL, "");
|
|
2706
|
-
}
|
|
2707
|
-
function paintOnly(line) {
|
|
2708
|
-
if (!HAS_CONTROL.test(line)) return line;
|
|
2709
|
-
return line.split(ESCAPE_SPLIT).map((piece) => piece.startsWith("\x1B") ? PAINT_ESCAPE.test(piece) ? piece : "" : piece.replace(CONTROL_IN_ROW, "")).join("");
|
|
2710
|
-
}
|
|
2711
|
-
function width(text) {
|
|
2712
|
-
return (0, import_string_width.default)(stripAnsi(text));
|
|
2713
|
-
}
|
|
2714
|
-
function truncate(text, max) {
|
|
2715
|
-
if (max <= 0) return "";
|
|
2716
|
-
if (width(text) <= max) return text;
|
|
2717
|
-
let out = "";
|
|
2718
|
-
let used = 0;
|
|
2719
|
-
for (const char of stripAnsi(text)) {
|
|
2720
|
-
const w = (0, import_string_width.default)(char);
|
|
2721
|
-
if (used + w > max - 1) break;
|
|
2722
|
-
out += char;
|
|
2723
|
-
used += w;
|
|
2724
|
-
}
|
|
2725
|
-
return out + "\u2026";
|
|
2726
|
-
}
|
|
2727
|
-
function pad(text, target) {
|
|
2728
|
-
const current = width(text);
|
|
2729
|
-
if (current > target) return truncate(text, target);
|
|
2730
|
-
return text + " ".repeat(target - current);
|
|
2731
|
-
}
|
|
2732
|
-
function chopWord(piece, max) {
|
|
2733
|
-
const chunks = [];
|
|
2734
|
-
let cur = "";
|
|
2735
|
-
let curWidth = 0;
|
|
2736
|
-
for (const ch of piece) {
|
|
2737
|
-
const w = (0, import_string_width.default)(ch);
|
|
2738
|
-
if (cur && curWidth + w > max) {
|
|
2739
|
-
chunks.push(cur);
|
|
2740
|
-
cur = ch;
|
|
2741
|
-
curWidth = w;
|
|
2742
|
-
} else {
|
|
2743
|
-
cur += ch;
|
|
2744
|
-
curWidth += w;
|
|
2745
|
-
}
|
|
2746
|
-
}
|
|
2747
|
-
chunks.push(cur);
|
|
2748
|
-
return chunks;
|
|
2749
|
-
}
|
|
2750
|
-
var wrapMemo = /* @__PURE__ */ new Map();
|
|
2751
|
-
var WRAP_MEMO_MAX = 64;
|
|
2752
|
-
function wrapLines(text, max) {
|
|
2753
|
-
const key = `${max}\0${text}`;
|
|
2754
|
-
const cached = wrapMemo.get(key);
|
|
2755
|
-
if (cached) {
|
|
2756
|
-
wrapMemo.delete(key);
|
|
2757
|
-
wrapMemo.set(key, cached);
|
|
2758
|
-
return cached;
|
|
2759
|
-
}
|
|
2760
|
-
const result = computeWrapLines(text, max);
|
|
2761
|
-
if (wrapMemo.size >= WRAP_MEMO_MAX) {
|
|
2762
|
-
const oldest = wrapMemo.keys().next().value;
|
|
2763
|
-
if (oldest !== void 0) wrapMemo.delete(oldest);
|
|
2764
|
-
}
|
|
2765
|
-
wrapMemo.set(key, result);
|
|
2766
|
-
return result;
|
|
2767
|
-
}
|
|
2768
|
-
function computeWrapLines(text, max) {
|
|
2769
|
-
if (max <= 0) return [{ line: "", start: 0 }];
|
|
2770
|
-
const out = [];
|
|
2771
|
-
let pos = 0;
|
|
2772
|
-
for (const paragraph of text.split("\n")) {
|
|
2773
|
-
let current = "";
|
|
2774
|
-
let currentStart = pos;
|
|
2775
|
-
let first = true;
|
|
2776
|
-
let wordPos = pos;
|
|
2777
|
-
for (const word of paragraph.split(" ")) {
|
|
2778
|
-
let piece = word;
|
|
2779
|
-
let pieceStart = wordPos;
|
|
2780
|
-
if (width(piece) > max) {
|
|
2781
|
-
const chunks = chopWord(piece, max);
|
|
2782
|
-
for (let ci = 0; ci < chunks.length - 1; ci++) {
|
|
2783
|
-
const head = chunks[ci];
|
|
2784
|
-
if (!first) {
|
|
2785
|
-
out.push({ line: current, start: currentStart });
|
|
2786
|
-
current = "";
|
|
2787
|
-
}
|
|
2788
|
-
out.push({ line: head, start: pieceStart });
|
|
2789
|
-
pieceStart += head.length;
|
|
2790
|
-
first = true;
|
|
2791
|
-
}
|
|
2792
|
-
piece = chunks[chunks.length - 1];
|
|
3301
|
+
const catalog = await fetchCatalog(api);
|
|
3302
|
+
const variants = effortsForTask(catalog.models, task);
|
|
3303
|
+
const current = task.assignedModel?.thinkingEffort;
|
|
3304
|
+
if (!level) {
|
|
3305
|
+
console.log(`
|
|
3306
|
+
Thinking effort \xB7 #${task.order} ${task.title}
|
|
3307
|
+
`);
|
|
3308
|
+
console.log(` ${current ? " " : "*"} default Let the executor choose`);
|
|
3309
|
+
for (const v of variants) {
|
|
3310
|
+
console.log(` ${v.id === current ? "*" : " "} ${v.id.padEnd(14)} ${v.label}`);
|
|
2793
3311
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
if (width(candidate) > max) {
|
|
2798
|
-
out.push({ line: current, start: currentStart });
|
|
2799
|
-
current = piece;
|
|
2800
|
-
currentStart = pieceStart;
|
|
2801
|
-
} else {
|
|
2802
|
-
current = candidate;
|
|
2803
|
-
currentStart = candidateStart;
|
|
3312
|
+
if (variants.length === 0) {
|
|
3313
|
+
console.log(`
|
|
3314
|
+
${task.assignedModel?.modelLabel ?? "This model"} exposes no effort levels.`);
|
|
2804
3315
|
}
|
|
2805
|
-
|
|
3316
|
+
console.log(`
|
|
3317
|
+
${EFFORT_USAGE2}`);
|
|
3318
|
+
return;
|
|
2806
3319
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3320
|
+
const wanted = level.toLowerCase();
|
|
3321
|
+
const thinkingEffort = wanted === "default" ? void 0 : wanted;
|
|
3322
|
+
if (thinkingEffort && !variants.some((v) => v.id === thinkingEffort)) {
|
|
3323
|
+
fail(
|
|
3324
|
+
variants.length > 0 ? `Unknown effort: ${level}. Available: ${variants.map((v) => v.id).join(", ")}, default.` : `${task.assignedModel?.modelLabel ?? "This task model"} exposes no effort levels.`
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
await api.updateTask(sessionId, task.id, {
|
|
3328
|
+
assignedModel: task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0,
|
|
3329
|
+
thinkingEffort: thinkingEffort ?? null
|
|
3330
|
+
});
|
|
3331
|
+
console.log(`Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`);
|
|
3332
|
+
});
|
|
2811
3333
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
3334
|
+
var MODE_USAGE = "Usage: ordewell task-mode <task-id-or-order> [<mode>] [--session-id <id>]";
|
|
3335
|
+
async function handleTaskMode(subArgs, injectedApi) {
|
|
3336
|
+
await withTask(subArgs, MODE_USAGE, injectedApi, async (api, sessionId, task, _tasks, mode) => {
|
|
3337
|
+
const catalog = await fetchCatalog(api);
|
|
3338
|
+
const modes = modesForTask(catalog.modesByRunner, task);
|
|
3339
|
+
if (!mode) {
|
|
3340
|
+
console.log(`
|
|
3341
|
+
Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
|
|
3342
|
+
`);
|
|
3343
|
+
if (modes.length === 0) {
|
|
3344
|
+
console.log(" This task has no runner, or its runner declares no modes.");
|
|
3345
|
+
}
|
|
3346
|
+
for (const m of modes) {
|
|
3347
|
+
console.log(` ${m.id === task.taskMode ? "*" : " "} ${m.id.padEnd(14)} ${m.label}${m.description ? ` \u2014 ${m.description}` : ""}`);
|
|
3348
|
+
}
|
|
3349
|
+
console.log(`
|
|
3350
|
+
${MODE_USAGE}`);
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
const chosen = modes.find((m) => m.id === mode);
|
|
3354
|
+
if (!chosen) {
|
|
3355
|
+
fail(
|
|
3356
|
+
modes.length > 0 ? `Unknown mode "${mode}" for ${task.assignedRunner}. Available: ${modes.map((m) => m.id).join(", ")}.` : `${task.assignedRunner ?? "This task"} declares no modes.`
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
await api.updateTask(sessionId, task.id, { taskMode: chosen.id });
|
|
3360
|
+
console.log(`Task #${task.order} mode set to ${chosen.label}.`);
|
|
3361
|
+
});
|
|
2814
3362
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
3363
|
+
var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
|
|
3364
|
+
async function handleTaskDeps(subArgs, injectedApi) {
|
|
3365
|
+
await withTask(subArgs, DEPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
|
|
3366
|
+
const candidates = (0, import_plan_utils2.dependencyCandidates)(tasks, task.id);
|
|
3367
|
+
if (!value) {
|
|
3368
|
+
console.log(`
|
|
3369
|
+
Dependencies \xB7 #${task.order} ${task.title}
|
|
3370
|
+
`);
|
|
3371
|
+
if (candidates.length === 0) {
|
|
3372
|
+
console.log(` Nothing runs before #${task.order}, so it has no possible dependencies.`);
|
|
3373
|
+
}
|
|
3374
|
+
for (const c of candidates) {
|
|
3375
|
+
const chosen = task.dependencies.includes(c.id);
|
|
3376
|
+
console.log(` ${chosen ? "*" : " "} ${c.id} #${c.order} ${c.title}${c.status === "completed" ? " (already completed)" : ""}`);
|
|
3377
|
+
}
|
|
3378
|
+
console.log(`
|
|
3379
|
+
* = current. ${DEPS_USAGE}`);
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
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);
|
|
3383
|
+
const check = (0, import_plan_utils2.canSetDependencies)(tasks, task.id, dependencies);
|
|
3384
|
+
if (!check.ok) fail(check.error ?? "Invalid dependencies.");
|
|
3385
|
+
await api.updateTask(sessionId, task.id, { dependencies });
|
|
3386
|
+
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.`);
|
|
3387
|
+
});
|
|
2817
3388
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
bold: colour("1"),
|
|
2822
|
-
dim: colour("2"),
|
|
2823
|
-
italic: colour("3"),
|
|
2824
|
-
underline: colour("4"),
|
|
2825
|
-
inverse: colour("7"),
|
|
2826
|
-
red: colour("31"),
|
|
2827
|
-
green: colour("32"),
|
|
2828
|
-
yellow: colour("33"),
|
|
2829
|
-
blue: colour("34"),
|
|
2830
|
-
magenta: colour("35"),
|
|
2831
|
-
cyan: colour("36"),
|
|
2832
|
-
grey: colour("90")
|
|
2833
|
-
};
|
|
3389
|
+
|
|
3390
|
+
// src/tui/index.ts
|
|
3391
|
+
var import_core20 = require("@ordewell/core");
|
|
2834
3392
|
|
|
2835
3393
|
// src/tui/editor.ts
|
|
2836
3394
|
function emptyEditor() {
|
|
@@ -2968,6 +3526,7 @@ var SLASH_COMMANDS = [
|
|
|
2968
3526
|
{ name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
|
|
2969
3527
|
{ name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
|
|
2970
3528
|
{ name: "stop", usage: "/stop", description: "Stop the planner or the running execution, whichever is active", category: "planning" },
|
|
3529
|
+
{ name: "handoff", usage: "/handoff [review|merge|discard|cleanup]", description: "Land an isolated run's branch: review it, merge it into yours, or discard it", category: "planning" },
|
|
2971
3530
|
// Tasks
|
|
2972
3531
|
{ name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
|
|
2973
3532
|
{ name: "remove-task", usage: "/remove-task <id>", description: "Remove a task from the plan", category: "tasks" },
|
|
@@ -2997,6 +3556,9 @@ var SLASH_COMMANDS = [
|
|
|
2997
3556
|
{ name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
|
|
2998
3557
|
// Sessions
|
|
2999
3558
|
{ name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
|
|
3559
|
+
{ name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
|
|
3560
|
+
{ name: "compact", usage: "/compact", description: "Condense this conversation into a summary; the last two exchanges and all tasks are kept", category: "session" },
|
|
3561
|
+
{ name: "rewind", usage: "/rewind [<message>]", description: "Fork the conversation from just before one of your messages, after a confirmation (the original is kept)", category: "session" },
|
|
3000
3562
|
{ name: "new", usage: "/new", description: "Start a new session", category: "session" },
|
|
3001
3563
|
{ name: "save", usage: "/save", description: "Save the current session", category: "session" },
|
|
3002
3564
|
{ name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
|
|
@@ -3268,6 +3830,7 @@ function initialState(overrides = {}) {
|
|
|
3268
3830
|
skills: noSkills(),
|
|
3269
3831
|
runners: [],
|
|
3270
3832
|
sessions: [],
|
|
3833
|
+
rewindTargets: null,
|
|
3271
3834
|
models: [],
|
|
3272
3835
|
modesByRunner: {},
|
|
3273
3836
|
orchestratorModels: [],
|
|
@@ -3281,6 +3844,7 @@ function initialState(overrides = {}) {
|
|
|
3281
3844
|
mouseCapture: true,
|
|
3282
3845
|
selection: null,
|
|
3283
3846
|
workspace: process.cwd(),
|
|
3847
|
+
handoff: null,
|
|
3284
3848
|
overlay: null,
|
|
3285
3849
|
pendingApprovals: [],
|
|
3286
3850
|
toast: "",
|
|
@@ -3293,7 +3857,7 @@ function initialState(overrides = {}) {
|
|
|
3293
3857
|
}
|
|
3294
3858
|
|
|
3295
3859
|
// src/tui/layout.ts
|
|
3296
|
-
var
|
|
3860
|
+
var import_core16 = require("@ordewell/core");
|
|
3297
3861
|
var CHROME_ROWS = 4;
|
|
3298
3862
|
function chatInputWrap(state) {
|
|
3299
3863
|
const active = state.focus === "chat" && !state.overlay;
|
|
@@ -3301,7 +3865,9 @@ function chatInputWrap(state) {
|
|
|
3301
3865
|
return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
|
|
3302
3866
|
}
|
|
3303
3867
|
function footerHints(state) {
|
|
3304
|
-
const
|
|
3868
|
+
const selected = selectedPlanRow(state)?.task;
|
|
3869
|
+
const markHint = selected?.status === "completed" ? "m undone" : "m done";
|
|
3870
|
+
const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
|
|
3305
3871
|
const planning = state.status === "planning" || state.status === "researching";
|
|
3306
3872
|
const escHint = planning ? "esc stop planning" : null;
|
|
3307
3873
|
if (state.focus === "plan") {
|
|
@@ -3324,6 +3890,7 @@ function footerHints(state) {
|
|
|
3324
3890
|
"a add",
|
|
3325
3891
|
"d remove",
|
|
3326
3892
|
"t terminal",
|
|
3893
|
+
...resolveHint,
|
|
3327
3894
|
"pgup/pgdn scroll",
|
|
3328
3895
|
"tab chat"
|
|
3329
3896
|
];
|
|
@@ -3417,21 +3984,18 @@ function chatBodyLines(messages, cols) {
|
|
|
3417
3984
|
}
|
|
3418
3985
|
function chatLayout(state, rows, cols) {
|
|
3419
3986
|
const welcome = state.tasks.length === 0;
|
|
3420
|
-
if (welcome && state.messages.length === 0) {
|
|
3421
|
-
return { lines: welcomeLines(state, cols), anchor: "top", maxScroll: 0 };
|
|
3422
|
-
}
|
|
3423
3987
|
const body = chatBodyLines(state.messages, cols);
|
|
3424
|
-
const lines = welcome ? [...welcomeLines(state, cols), "", ...body]
|
|
3425
|
-
return { lines, anchor: "bottom", maxScroll: Math.max(0, lines.length - rows) };
|
|
3988
|
+
const lines = !welcome ? body : state.messages.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
|
|
3989
|
+
return { lines, anchor: lines.length > rows ? "bottom" : "top", maxScroll: Math.max(0, lines.length - rows) };
|
|
3426
3990
|
}
|
|
3427
3991
|
function chatScrollMax(state) {
|
|
3428
3992
|
return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
|
|
3429
3993
|
}
|
|
3430
3994
|
function plannerSetup(state) {
|
|
3431
3995
|
const provider = state.plannerProvider;
|
|
3432
|
-
const registration =
|
|
3996
|
+
const registration = import_core16.ALL_PROVIDERS[provider];
|
|
3433
3997
|
if (!registration) return { label: "not set", ready: false };
|
|
3434
|
-
const runner = (0,
|
|
3998
|
+
const runner = (0, import_core16.runnerForProvider)(provider);
|
|
3435
3999
|
const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
|
|
3436
4000
|
return { label: registration.label, ready };
|
|
3437
4001
|
}
|
|
@@ -3539,33 +4103,69 @@ var STATUS_ICON = {
|
|
|
3539
4103
|
pending: "\xB7"
|
|
3540
4104
|
};
|
|
3541
4105
|
var IDLE_ICON = "~";
|
|
4106
|
+
var FIRST_ROW_LINE = 2;
|
|
3542
4107
|
function planLayout(state, rows, cols) {
|
|
3543
4108
|
const done = state.tasks.filter((t) => t.status === "completed").length;
|
|
3544
4109
|
const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
|
|
3545
|
-
|
|
4110
|
+
const rowSpans = [];
|
|
3546
4111
|
let editorLine;
|
|
3547
4112
|
planRows(state).forEach((row, i) => {
|
|
3548
4113
|
const taskStart = lines.length;
|
|
3549
|
-
if (i === state.selectedTask) selectedLine = taskStart;
|
|
3550
4114
|
const renderedTask = taskLines(state, row, i, cols);
|
|
3551
4115
|
if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
|
|
3552
4116
|
editorLine = taskStart + renderedTask.editorLine;
|
|
3553
4117
|
}
|
|
3554
4118
|
lines.push(...renderedTask.lines);
|
|
4119
|
+
rowSpans.push({ start: taskStart, end: lines.length - 1 });
|
|
3555
4120
|
});
|
|
3556
|
-
const
|
|
3557
|
-
const
|
|
3558
|
-
return { lines,
|
|
4121
|
+
const selected = rowSpans[state.selectedTask] ?? { start: FIRST_ROW_LINE, end: FIRST_ROW_LINE };
|
|
4122
|
+
const anchor = editorLine === void 0 ? selected : { start: editorLine, end: editorLine };
|
|
4123
|
+
return { lines, rowSpans, anchor, rows, maxScroll: Math.max(0, lines.length - rows) };
|
|
4124
|
+
}
|
|
4125
|
+
var clampOffset = (layout, offset) => Math.max(0, Math.min(layout.maxScroll, offset));
|
|
4126
|
+
function revealOffset(layout, offset, span) {
|
|
4127
|
+
const at = clampOffset(layout, offset);
|
|
4128
|
+
const bodyHeight = layout.rows - 1;
|
|
4129
|
+
const first = span.start <= FIRST_ROW_LINE ? 0 : span.start - 1;
|
|
4130
|
+
if (span.start < 1 + at || span.end - span.start + 1 > bodyHeight) return clampOffset(layout, first);
|
|
4131
|
+
if (span.end > at + layout.rows - 1) return clampOffset(layout, span.end - layout.rows + 1);
|
|
4132
|
+
return at;
|
|
3559
4133
|
}
|
|
3560
4134
|
function planOffset(layout, planScroll) {
|
|
3561
|
-
if (planScroll === null) return layout.
|
|
3562
|
-
return
|
|
4135
|
+
if (planScroll === null) return revealOffset(layout, 0, layout.anchor);
|
|
4136
|
+
return clampOffset(layout, planScroll);
|
|
4137
|
+
}
|
|
4138
|
+
function topRowInView(layout, offset) {
|
|
4139
|
+
const top = 1 + offset;
|
|
4140
|
+
const whole = layout.rowSpans.findIndex((span) => span.start >= top && span.end <= offset + layout.rows - 1);
|
|
4141
|
+
if (whole >= 0) return whole;
|
|
4142
|
+
const covering = layout.rowSpans.findIndex((span) => span.end >= top);
|
|
4143
|
+
return covering >= 0 ? covering : layout.rowSpans.length - 1;
|
|
4144
|
+
}
|
|
4145
|
+
function bottomRowInView(layout, offset) {
|
|
4146
|
+
const bottom = offset + layout.rows - 1;
|
|
4147
|
+
const spans = layout.rowSpans;
|
|
4148
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
4149
|
+
if (spans[i].start >= 1 + offset && spans[i].end <= bottom) return i;
|
|
4150
|
+
}
|
|
4151
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
4152
|
+
if (spans[i].start <= bottom) return i;
|
|
4153
|
+
}
|
|
4154
|
+
return 0;
|
|
4155
|
+
}
|
|
4156
|
+
function rowNearestView(layout, offset, row) {
|
|
4157
|
+
const span = layout.rowSpans[row];
|
|
4158
|
+
if (!span) return row;
|
|
4159
|
+
if (span.end < 1 + offset) return topRowInView(layout, offset);
|
|
4160
|
+
if (span.start > offset + layout.rows - 1) return bottomRowInView(layout, offset);
|
|
4161
|
+
return row;
|
|
3563
4162
|
}
|
|
3564
4163
|
function planScrollExtent(state) {
|
|
3565
4164
|
const cols = planPaneWidth(state);
|
|
3566
|
-
if (cols === 0)
|
|
3567
|
-
|
|
3568
|
-
|
|
4165
|
+
if (cols === 0) {
|
|
4166
|
+
return { lines: [], rowSpans: [], anchor: { start: FIRST_ROW_LINE, end: FIRST_ROW_LINE }, rows: 0, maxScroll: 0 };
|
|
4167
|
+
}
|
|
4168
|
+
return planLayout(state, bodyRows(state), cols);
|
|
3569
4169
|
}
|
|
3570
4170
|
function taskLines(state, row, index, cols) {
|
|
3571
4171
|
const { task, parent } = row;
|
|
@@ -3578,7 +4178,7 @@ function taskLines(state, row, index, cols) {
|
|
|
3578
4178
|
const icon = paint(rawIcon);
|
|
3579
4179
|
const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
|
|
3580
4180
|
const caret = selected ? style.cyan("\u276F") : " ";
|
|
3581
|
-
const orderLabel = parent ? (0,
|
|
4181
|
+
const orderLabel = parent ? (0, import_core16.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
|
|
3582
4182
|
const indent = parent ? " " : "";
|
|
3583
4183
|
const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
|
|
3584
4184
|
const titleRoom = Math.max(1, cols - width(head));
|
|
@@ -3602,9 +4202,30 @@ function taskLines(state, row, index, cols) {
|
|
|
3602
4202
|
const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
|
|
3603
4203
|
if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
|
|
3604
4204
|
if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
|
|
4205
|
+
if (task.isolation?.state === "conflict") {
|
|
4206
|
+
const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
|
|
4207
|
+
const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)})` : "";
|
|
4208
|
+
lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
|
|
4209
|
+
}
|
|
4210
|
+
if (task.isolation?.state === "repairing") {
|
|
4211
|
+
const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)}` : "";
|
|
4212
|
+
const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
|
|
4213
|
+
lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
|
|
4214
|
+
}
|
|
4215
|
+
if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
|
|
4216
|
+
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core16.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
|
|
4217
|
+
}
|
|
3605
4218
|
let editorLine;
|
|
3606
4219
|
if (expanded) {
|
|
3607
4220
|
lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
|
|
4221
|
+
if (task.isolation && task.isolation.state !== "none" && task.isolation.branch) {
|
|
4222
|
+
lines.push(...taskText("Branch", task.isolation.branch, cols, bodyPad));
|
|
4223
|
+
const repos = taskRepoNames(task.isolation);
|
|
4224
|
+
if (repos.length > 0) lines.push(...taskText("Repos", repos.join(", "), cols, bodyPad));
|
|
4225
|
+
if (task.isolation.state !== "integrated" && task.isolation.worktree) {
|
|
4226
|
+
lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
3608
4229
|
if (modeInfo?.autonomous) {
|
|
3609
4230
|
lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
|
|
3610
4231
|
}
|
|
@@ -3677,6 +4298,7 @@ function helpScrollMax(state) {
|
|
|
3677
4298
|
}
|
|
3678
4299
|
|
|
3679
4300
|
// src/tui/render.ts
|
|
4301
|
+
var import_core17 = require("@ordewell/core");
|
|
3680
4302
|
function render(state) {
|
|
3681
4303
|
const { rows, cols } = state;
|
|
3682
4304
|
const rowsForBody = bodyRows(state);
|
|
@@ -3775,8 +4397,7 @@ function renderSkills(state, cols) {
|
|
|
3775
4397
|
return truncate([...badges, auto].join(" "), cols);
|
|
3776
4398
|
}
|
|
3777
4399
|
function scrolledBackMark(state) {
|
|
3778
|
-
|
|
3779
|
-
return state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
|
|
4400
|
+
return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
|
|
3780
4401
|
}
|
|
3781
4402
|
function renderStatus(state, cols) {
|
|
3782
4403
|
const mark = scrolledBackMark(state);
|
|
@@ -3916,14 +4537,7 @@ function renderOverlay(state, rows, cols) {
|
|
|
3916
4537
|
cols
|
|
3917
4538
|
);
|
|
3918
4539
|
}
|
|
3919
|
-
if (overlay.kind === "confirm")
|
|
3920
|
-
return frame(
|
|
3921
|
-
overlay.title,
|
|
3922
|
-
[overlay.message, "", style.grey("enter confirms \xB7 esc cancels")],
|
|
3923
|
-
rows,
|
|
3924
|
-
cols
|
|
3925
|
-
);
|
|
3926
|
-
}
|
|
4540
|
+
if (overlay.kind === "confirm") return renderConfirm(overlay, rows, cols);
|
|
3927
4541
|
if (overlay.kind === "prompt") {
|
|
3928
4542
|
const secret = overlay.action.kind === "api-key";
|
|
3929
4543
|
const shown = secret ? "\u2022".repeat(overlay.value.length) : overlay.value;
|
|
@@ -3934,8 +4548,54 @@ function renderOverlay(state, rows, cols) {
|
|
|
3934
4548
|
cols
|
|
3935
4549
|
);
|
|
3936
4550
|
}
|
|
4551
|
+
if (overlay.kind === "handoff") return renderHandoff(state, overlay, rows, cols);
|
|
3937
4552
|
return renderPicker(overlay.picker, rows, cols);
|
|
3938
4553
|
}
|
|
4554
|
+
function renderHandoff(state, overlay, rows, cols) {
|
|
4555
|
+
const handoff = state.handoff;
|
|
4556
|
+
if (!handoff) return frame("Handoff", [style.grey("Nothing to hand off.")], rows, cols);
|
|
4557
|
+
if (overlay.diff) {
|
|
4558
|
+
const room = diffRoom(rows);
|
|
4559
|
+
const { lines, scroll } = overlay.diff;
|
|
4560
|
+
const shown = lines.slice(scroll, scroll + room).map((line) => paintDiffLine(truncate(line, Math.max(1, cols - 2))));
|
|
4561
|
+
const more = scroll + room < lines.length ? "\u2191\u2193 pgup/pgdn scroll \xB7 " : "";
|
|
4562
|
+
return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
|
|
4563
|
+
}
|
|
4564
|
+
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
|
|
4565
|
+
const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core17.capConflictFiles)(task.repairedFiles)})`) : "";
|
|
4566
|
+
return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
|
|
4567
|
+
});
|
|
4568
|
+
const actions = handoffActions(handoff).map((action, index) => {
|
|
4569
|
+
const active = index === overlay.index;
|
|
4570
|
+
const caret = active ? style.cyan("\u276F") : " ";
|
|
4571
|
+
return `${caret} ${active ? style.bold(action.label) : action.label}${style.grey(` \u2014 ${action.hint}`)}`;
|
|
4572
|
+
});
|
|
4573
|
+
const perRepo = isRepoGroup(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
|
|
4574
|
+
return frame(
|
|
4575
|
+
"Run handoff",
|
|
4576
|
+
[
|
|
4577
|
+
`Branch ${style.cyan(handoffBranch(handoff))}`,
|
|
4578
|
+
style.grey(`Forked from ${handoffBase(handoff, 12)}`),
|
|
4579
|
+
"",
|
|
4580
|
+
...perRepo,
|
|
4581
|
+
"Landed on it, in plan order:",
|
|
4582
|
+
...landed,
|
|
4583
|
+
"",
|
|
4584
|
+
...actions,
|
|
4585
|
+
"",
|
|
4586
|
+
style.grey("\u2191\u2193 choose \xB7 enter selects \xB7 esc closes")
|
|
4587
|
+
],
|
|
4588
|
+
rows,
|
|
4589
|
+
cols
|
|
4590
|
+
);
|
|
4591
|
+
}
|
|
4592
|
+
function paintDiffLine(line) {
|
|
4593
|
+
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff ")) return style.bold(line);
|
|
4594
|
+
if (line.startsWith("+")) return style.green(line);
|
|
4595
|
+
if (line.startsWith("-")) return style.red(line);
|
|
4596
|
+
if (line.startsWith("@@")) return style.cyan(line);
|
|
4597
|
+
return line;
|
|
4598
|
+
}
|
|
3939
4599
|
function renderPicker(picker2, rows, cols) {
|
|
3940
4600
|
const items = visibleItems(picker2);
|
|
3941
4601
|
const body = [];
|
|
@@ -3964,6 +4624,24 @@ function renderHelp(scroll, rows, cols) {
|
|
|
3964
4624
|
const footer = style.grey(offset < maxScroll ? "\u2191\u2193 scroll \xB7 any other key closes" : "any key closes");
|
|
3965
4625
|
return frame("Commands", [...lines.slice(offset, offset + room), footer], rows, cols);
|
|
3966
4626
|
}
|
|
4627
|
+
var QUOTE_MAX_LINES = 8;
|
|
4628
|
+
function renderConfirm(overlay, rows, cols) {
|
|
4629
|
+
const { choice, quote, note } = overlay;
|
|
4630
|
+
const inner = Math.max(1, cols - 3);
|
|
4631
|
+
const options = (choice?.options ?? []).map((o, i) => `${i === choice?.index ? style.cyan("\u276F") : " "} ${i + 1}. ${o.label}`);
|
|
4632
|
+
const hint = choice ? style.grey(`\u2191\u2193 move \xB7 enter or 1\u2013${choice.options.length} chooses \xB7 esc cancels`) : style.grey("enter confirms \xB7 esc cancels");
|
|
4633
|
+
const noteLines = note ? ["", ...note.split("\n")] : [];
|
|
4634
|
+
const rest = [...noteLines, ...options.length > 0 ? ["", ...options] : [], "", hint];
|
|
4635
|
+
const quoted = [];
|
|
4636
|
+
if (quote !== void 0) {
|
|
4637
|
+
const fixed = 1 + wrap(overlay.message, inner).length + 1 + 1 + rest.flatMap((l) => wrap(l, inner)).length;
|
|
4638
|
+
const room = Math.max(2, Math.min(QUOTE_MAX_LINES, rows - fixed));
|
|
4639
|
+
const wrapped = wrap(quote, inner - 2);
|
|
4640
|
+
const shown = wrapped.length > room ? [...wrapped.slice(0, room - 1), "\u2026"] : wrapped;
|
|
4641
|
+
quoted.push("", ...shown.map((l) => `${style.grey("\u2502")} ${l}`));
|
|
4642
|
+
}
|
|
4643
|
+
return frame(overlay.title, [overlay.message, ...quoted, ...rest], rows, cols);
|
|
4644
|
+
}
|
|
3967
4645
|
function frame(title, body, rows, cols) {
|
|
3968
4646
|
const inner = Math.max(1, cols - 2);
|
|
3969
4647
|
const lines = [
|
|
@@ -3974,26 +4652,16 @@ function frame(title, body, rows, cols) {
|
|
|
3974
4652
|
}
|
|
3975
4653
|
|
|
3976
4654
|
// src/tui/reducer.ts
|
|
3977
|
-
var
|
|
4655
|
+
var import_core18 = require("@ordewell/core");
|
|
3978
4656
|
var import_plan_utils3 = require("@ordewell/core/plan-utils");
|
|
3979
|
-
var KNOWN_PROVIDERS2 = Object.keys(
|
|
4657
|
+
var KNOWN_PROVIDERS2 = Object.keys(import_core18.ALL_PROVIDERS);
|
|
3980
4658
|
var DEFAULT_EFFORT = "__runner_default__";
|
|
3981
4659
|
function step(state, effects = []) {
|
|
3982
4660
|
return { state, effects };
|
|
3983
4661
|
}
|
|
3984
|
-
function say(state, role, content, research, extra) {
|
|
3985
|
-
const message = {
|
|
3986
|
-
role,
|
|
3987
|
-
content: sanitize(content),
|
|
3988
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3989
|
-
...research ? { research } : {},
|
|
3990
|
-
...extra
|
|
3991
|
-
};
|
|
3992
|
-
return { ...state, messages: [...state.messages, message], scroll: 0 };
|
|
3993
|
-
}
|
|
3994
4662
|
function restoredMessages(history) {
|
|
3995
4663
|
return history.map((entry) => ({
|
|
3996
|
-
role: entry.kind === "plan_generated"
|
|
4664
|
+
role: entry.kind === "plan_generated" || entry.kind === "system" || entry.kind === "compaction" ? "system" : entry.role,
|
|
3997
4665
|
content: sanitize(entry.kind === "plan_generated" ? "Plan generated." : entry.content),
|
|
3998
4666
|
timestamp: entry.timestamp
|
|
3999
4667
|
}));
|
|
@@ -4001,7 +4669,9 @@ function restoredMessages(history) {
|
|
|
4001
4669
|
var isPendingResearch = (m) => m.role === "research" && !m.research?.outcome;
|
|
4002
4670
|
function alreadySpoken(messages, content) {
|
|
4003
4671
|
const last = findLastIndex(messages, (m) => m.role === "assistant" || m.role === "user");
|
|
4004
|
-
|
|
4672
|
+
const spoken = last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
|
|
4673
|
+
const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
|
|
4674
|
+
return spoken || redrawn;
|
|
4005
4675
|
}
|
|
4006
4676
|
function researchLabel(messages, summary) {
|
|
4007
4677
|
const others = messages.filter(isPendingResearch).length - 1;
|
|
@@ -4025,20 +4695,22 @@ function findLastIndex(items, match) {
|
|
|
4025
4695
|
return -1;
|
|
4026
4696
|
}
|
|
4027
4697
|
var THINKING_TAIL = 400;
|
|
4028
|
-
var
|
|
4698
|
+
var fail3 = (state, message) => step(say(state, "error", message));
|
|
4029
4699
|
var stale = (state, sessionId) => sessionId !== void 0 && sessionId !== state.sessionId;
|
|
4030
4700
|
function reduce(state, action) {
|
|
4031
4701
|
switch (action.type) {
|
|
4032
4702
|
case "key":
|
|
4033
4703
|
return handleKey2(state, action.key);
|
|
4704
|
+
// Isolation belongs to one session's plan, and task ids repeat across
|
|
4705
|
+
// sessions, so nothing of the last one's may carry over into this.
|
|
4034
4706
|
case "sessionStarted":
|
|
4035
|
-
return step({ ...state, sessionId: action.sessionId, goal: action.goal });
|
|
4707
|
+
return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
|
|
4036
4708
|
// The daemon registers a session only once planning succeeds; when it does
|
|
4037
4709
|
// not, holding on to the id would send every next message to a session the
|
|
4038
4710
|
// server never had.
|
|
4039
4711
|
case "sessionCleared":
|
|
4040
4712
|
return step({
|
|
4041
|
-
...state,
|
|
4713
|
+
...clearIsolation(state),
|
|
4042
4714
|
sessionId: null,
|
|
4043
4715
|
goal: "",
|
|
4044
4716
|
pendingApprovals: [],
|
|
@@ -4057,18 +4729,21 @@ function reduce(state, action) {
|
|
|
4057
4729
|
return step({ ...state, messages: restoredMessages(action.history), scroll: 0 });
|
|
4058
4730
|
case "planUpdated": {
|
|
4059
4731
|
if (stale(state, action.sessionId)) return step(state);
|
|
4060
|
-
const
|
|
4732
|
+
const isolation = isolationOfPlan(action.plan);
|
|
4733
|
+
const tasks = isolationForPlan(normalizeTasks(action.plan), isolation, state.tasks);
|
|
4061
4734
|
const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
|
|
4062
4735
|
const next = {
|
|
4063
4736
|
...state,
|
|
4064
4737
|
tasks,
|
|
4738
|
+
handoff: isolation ? isolation.handoff : state.handoff,
|
|
4065
4739
|
status: state.status === "planning" || state.status === "researching" ? "idle" : state.status,
|
|
4066
4740
|
busyLabel: "",
|
|
4067
4741
|
thinkingLine: "",
|
|
4068
4742
|
expandedTaskId,
|
|
4069
4743
|
taskEditor: expandedTaskId !== null ? state.taskEditor : null
|
|
4070
4744
|
};
|
|
4071
|
-
|
|
4745
|
+
const selectedTask = clampSelection(state.selectedTask, planRows(next).length);
|
|
4746
|
+
return step(settlePlan({ ...next, selectedTask, focus: tasks.length === 0 ? "chat" : state.focus }));
|
|
4072
4747
|
}
|
|
4073
4748
|
case "plannerMessage": {
|
|
4074
4749
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -4133,9 +4808,10 @@ function reduce(state, action) {
|
|
|
4133
4808
|
const update = updates[t.id];
|
|
4134
4809
|
if (update === void 0) return t;
|
|
4135
4810
|
const idleSince = update.idleSince ?? null;
|
|
4136
|
-
|
|
4811
|
+
const isolation = update.isolation ?? t.isolation;
|
|
4812
|
+
if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
|
|
4137
4813
|
changed = true;
|
|
4138
|
-
return { ...t, status: update.status, idleSince };
|
|
4814
|
+
return { ...t, status: update.status, idleSince, isolation };
|
|
4139
4815
|
}
|
|
4140
4816
|
return t;
|
|
4141
4817
|
});
|
|
@@ -4148,6 +4824,23 @@ function reduce(state, action) {
|
|
|
4148
4824
|
if (sessionId === null) return step(state);
|
|
4149
4825
|
return step(state, [{ type: "processQueued", sessionId }]);
|
|
4150
4826
|
}
|
|
4827
|
+
case "isolationBlocked": {
|
|
4828
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4829
|
+
return step(blockedPicker(state, action.message, action.repos));
|
|
4830
|
+
}
|
|
4831
|
+
case "isolationHandoff": {
|
|
4832
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4833
|
+
return step(handoffArrived(state, action.handoff));
|
|
4834
|
+
}
|
|
4835
|
+
case "handoffDiff": {
|
|
4836
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4837
|
+
return step(showDiff(state, action.diff));
|
|
4838
|
+
}
|
|
4839
|
+
case "runCleared": {
|
|
4840
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4841
|
+
const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
|
|
4842
|
+
return step({ ...clearIsolation(state), overlay });
|
|
4843
|
+
}
|
|
4151
4844
|
case "executionComplete": {
|
|
4152
4845
|
if (stale(state, action.sessionId)) return step(state);
|
|
4153
4846
|
const { completed, total, failed } = action.summary ?? {
|
|
@@ -4182,6 +4875,33 @@ function reduce(state, action) {
|
|
|
4182
4875
|
);
|
|
4183
4876
|
case "sessionsLoaded":
|
|
4184
4877
|
return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
|
|
4878
|
+
case "rewindTargetsLoaded":
|
|
4879
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4880
|
+
if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
|
|
4881
|
+
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
4882
|
+
// A fork holds no run, whatever the session it came from was doing — the
|
|
4883
|
+
// original keeps its run, and its events are stale from here on.
|
|
4884
|
+
case "sessionForked":
|
|
4885
|
+
return step({
|
|
4886
|
+
...clearIsolation(state),
|
|
4887
|
+
sessionId: action.sessionId,
|
|
4888
|
+
goal: action.goal,
|
|
4889
|
+
status: "idle",
|
|
4890
|
+
busyLabel: "",
|
|
4891
|
+
thinkingLine: "",
|
|
4892
|
+
planApproved: false,
|
|
4893
|
+
pendingApprovals: [],
|
|
4894
|
+
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
4895
|
+
});
|
|
4896
|
+
// The rewound message comes back as a draft to edit and resend, so it lands
|
|
4897
|
+
// as if just typed: history is not being browsed, and nothing is parked.
|
|
4898
|
+
case "inputPrefilled":
|
|
4899
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4900
|
+
return step({
|
|
4901
|
+
...state,
|
|
4902
|
+
focus: "chat",
|
|
4903
|
+
editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
4904
|
+
});
|
|
4185
4905
|
case "runnersLoaded":
|
|
4186
4906
|
return step(refillPicker({
|
|
4187
4907
|
...state,
|
|
@@ -4207,11 +4927,7 @@ function reduce(state, action) {
|
|
|
4207
4927
|
return step(say(state, "system", action.message));
|
|
4208
4928
|
case "resize": {
|
|
4209
4929
|
const resized = { ...state, rows: action.rows, cols: action.cols, selection: null };
|
|
4210
|
-
return step({
|
|
4211
|
-
...resized,
|
|
4212
|
-
scroll: clamp(resized.scroll, chatScrollMax(resized)),
|
|
4213
|
-
planScroll: resized.planScroll === null ? null : clamp(resized.planScroll, planScrollExtent(resized).maxScroll)
|
|
4214
|
-
});
|
|
4930
|
+
return step(settlePlan({ ...resized, scroll: clamp(resized.scroll, chatScrollMax(resized)) }));
|
|
4215
4931
|
}
|
|
4216
4932
|
// Whether there is anything to animate is the app loop's call (it owns the
|
|
4217
4933
|
// timer); a tick that arrives simply advances the frame.
|
|
@@ -4286,11 +5002,11 @@ function modelDetail(m) {
|
|
|
4286
5002
|
function providerErrorHint(state) {
|
|
4287
5003
|
const failed = Object.keys(state.providerErrors ?? {});
|
|
4288
5004
|
if (failed.length === 0) return void 0;
|
|
4289
|
-
const names = failed.map((p) =>
|
|
5005
|
+
const names = failed.map((p) => import_core18.ALL_PROVIDERS[p]?.label ?? p);
|
|
4290
5006
|
return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
|
|
4291
5007
|
}
|
|
4292
5008
|
function plannerModelItems(state) {
|
|
4293
|
-
const runner = (0,
|
|
5009
|
+
const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
|
|
4294
5010
|
if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
|
|
4295
5011
|
const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
|
|
4296
5012
|
id: m.id,
|
|
@@ -4304,7 +5020,7 @@ function plannerModelItems(state) {
|
|
|
4304
5020
|
return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
|
|
4305
5021
|
}
|
|
4306
5022
|
function plannerEffortItems(state) {
|
|
4307
|
-
const runner = (0,
|
|
5023
|
+
const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
|
|
4308
5024
|
if (!runner) {
|
|
4309
5025
|
return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
|
|
4310
5026
|
}
|
|
@@ -4331,6 +5047,13 @@ function pickerItemsFor(state, action) {
|
|
|
4331
5047
|
if (action.kind === "set-planner-effort") {
|
|
4332
5048
|
return plannerEffortItems(state);
|
|
4333
5049
|
}
|
|
5050
|
+
if (action.kind === "rewind") {
|
|
5051
|
+
if (state.rewindTargets === null) return [];
|
|
5052
|
+
if (state.rewindTargets.length === 0) {
|
|
5053
|
+
return [{ id: "", label: "Nothing to rewind to yet", detail: "the only message so far is the goal", disabled: true }];
|
|
5054
|
+
}
|
|
5055
|
+
return [...state.rewindTargets].reverse().map((t) => ({ id: String(t.index), label: t.preview, detail: `message ${t.index}` }));
|
|
5056
|
+
}
|
|
4334
5057
|
if (action.kind === "load-session" || action.kind === "delete-session") {
|
|
4335
5058
|
return state.sessions.map((s) => ({
|
|
4336
5059
|
id: s.id,
|
|
@@ -4422,7 +5145,7 @@ function pointerPane(state, key) {
|
|
|
4422
5145
|
}
|
|
4423
5146
|
function scrollPointed(state, key) {
|
|
4424
5147
|
const delta = key.name === "scrollup" ? WHEEL_NOTCH : -WHEEL_NOTCH;
|
|
4425
|
-
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta) : scrollChat(state, delta);
|
|
5148
|
+
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta, false) : scrollChat(state, delta);
|
|
4426
5149
|
}
|
|
4427
5150
|
var isMouseSelect = (key) => key.name === "mousedown" || key.name === "mousedrag" || key.name === "mouseup";
|
|
4428
5151
|
function selectionCell(state, pane, key) {
|
|
@@ -4455,10 +5178,23 @@ function clamp(value, max) {
|
|
|
4455
5178
|
function scrollChat(state, delta) {
|
|
4456
5179
|
return step({ ...state, scroll: clamp(state.scroll + delta, chatScrollMax(state)) });
|
|
4457
5180
|
}
|
|
4458
|
-
function
|
|
4459
|
-
|
|
4460
|
-
const
|
|
4461
|
-
return
|
|
5181
|
+
function settlePlan(state) {
|
|
5182
|
+
if (planPaneWidth(state) === 0) return state;
|
|
5183
|
+
const layout = planScrollExtent(state);
|
|
5184
|
+
return { ...state, planScroll: revealOffset(layout, planOffset(layout, state.planScroll), layout.anchor) };
|
|
5185
|
+
}
|
|
5186
|
+
function scrollPlan(state, delta, paged) {
|
|
5187
|
+
const layout = planScrollExtent(state);
|
|
5188
|
+
const from = planOffset(layout, state.planScroll);
|
|
5189
|
+
const to = clamp(from - delta, layout.maxScroll);
|
|
5190
|
+
const scrolled = { ...state, planScroll: to };
|
|
5191
|
+
if (state.taskEditor || layout.rowSpans.length === 0) return step(scrolled);
|
|
5192
|
+
const back = delta > 0;
|
|
5193
|
+
if (!paged) {
|
|
5194
|
+
return step({ ...scrolled, selectedTask: rowNearestView(layout, to, state.selectedTask) });
|
|
5195
|
+
}
|
|
5196
|
+
if (to === from) return step({ ...scrolled, selectedTask: back ? 0 : layout.rowSpans.length - 1 });
|
|
5197
|
+
return step({ ...scrolled, selectedTask: back ? bottomRowInView(layout, to) : topRowInView(layout, to) });
|
|
4462
5198
|
}
|
|
4463
5199
|
var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
|
|
4464
5200
|
function stopPlanning(state, sessionId) {
|
|
@@ -4488,7 +5224,10 @@ function handleKey2(state, key) {
|
|
|
4488
5224
|
return step({ ...state, editor: { ...state.editor, text, cursor: token.start + completed.length } });
|
|
4489
5225
|
}
|
|
4490
5226
|
}
|
|
4491
|
-
if (key.name === "tab")
|
|
5227
|
+
if (key.name === "tab") {
|
|
5228
|
+
if (state.focus === "chat" && state.tasks.length === 0) return step(state);
|
|
5229
|
+
return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
|
|
5230
|
+
}
|
|
4492
5231
|
if (state.focus === "plan") return handlePlanKey(state, key);
|
|
4493
5232
|
if (key.name === "enter") return submit(state);
|
|
4494
5233
|
if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
@@ -4520,35 +5259,32 @@ function handlePlanKey(state, key) {
|
|
|
4520
5259
|
const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
|
|
4521
5260
|
if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
|
|
4522
5261
|
if (key.name === "escape") {
|
|
4523
|
-
if (state.expandedTaskId) return step({ ...state, expandedTaskId: null
|
|
5262
|
+
if (state.expandedTaskId) return step(settlePlan({ ...state, expandedTaskId: null }));
|
|
4524
5263
|
return step({ ...state, focus: "chat" });
|
|
4525
5264
|
}
|
|
4526
5265
|
if (key.name === "up") {
|
|
4527
|
-
return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1)
|
|
5266
|
+
return step(settlePlan({ ...state, selectedTask: Math.max(0, state.selectedTask - 1) }));
|
|
4528
5267
|
}
|
|
4529
5268
|
if (key.name === "down") {
|
|
4530
|
-
return step({
|
|
4531
|
-
...state,
|
|
4532
|
-
selectedTask: Math.min(planRows(state).length - 1, state.selectedTask + 1),
|
|
4533
|
-
planScroll: null
|
|
4534
|
-
});
|
|
5269
|
+
return step(settlePlan({ ...state, selectedTask: clampSelection(state.selectedTask + 1, planRows(state).length) }));
|
|
4535
5270
|
}
|
|
4536
5271
|
const scroll = scrollDelta(key, state);
|
|
4537
|
-
if (scroll !== null) return scrollPlan(state, scroll);
|
|
5272
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5273
|
+
if (key.name === "char" && key.char === "a") return addTask(state, "");
|
|
4538
5274
|
const row = selectedPlanRow(state);
|
|
4539
5275
|
if (!row) return step(state);
|
|
4540
5276
|
const task = row.task;
|
|
4541
5277
|
if (key.name === "enter" || key.name === "right") {
|
|
4542
5278
|
const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
|
|
4543
5279
|
if (hasSubtasks && state.expandedTaskId !== task.id) {
|
|
4544
|
-
return step({ ...state, expandedTaskId: task.id, taskEditor: null
|
|
5280
|
+
return step(settlePlan({ ...state, expandedTaskId: task.id, taskEditor: null }));
|
|
4545
5281
|
}
|
|
4546
5282
|
const text = task.prompt ?? task.description ?? task.title;
|
|
4547
|
-
return step({
|
|
5283
|
+
return step(settlePlan({
|
|
4548
5284
|
...state,
|
|
4549
5285
|
expandedTaskId: task.id,
|
|
4550
5286
|
taskEditor: { ...emptyEditor(), text, cursor: text.length }
|
|
4551
|
-
});
|
|
5287
|
+
}));
|
|
4552
5288
|
}
|
|
4553
5289
|
if (!state.sessionId || key.name !== "char") return step(state);
|
|
4554
5290
|
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
@@ -4557,10 +5293,12 @@ function handlePlanKey(state, key) {
|
|
|
4557
5293
|
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
4558
5294
|
}
|
|
4559
5295
|
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
4560
|
-
if (key.char === "a") return addTask(state, "");
|
|
4561
5296
|
if (key.char === "t") {
|
|
4562
5297
|
return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
|
|
4563
5298
|
}
|
|
5299
|
+
if (key.char === "x" && task.isolation?.state === "conflict") {
|
|
5300
|
+
return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
|
|
5301
|
+
}
|
|
4564
5302
|
if (key.char === "R") return openTaskRunnerPicker(state, task);
|
|
4565
5303
|
if (key.char === "o") return openTaskModelPicker(state, task);
|
|
4566
5304
|
if (key.char === "e") return openTaskEffortPicker(state, task);
|
|
@@ -4570,13 +5308,13 @@ function handlePlanKey(state, key) {
|
|
|
4570
5308
|
}
|
|
4571
5309
|
function handleTaskEditKey(state, task, editor, key) {
|
|
4572
5310
|
if (key.name === "enter") return commitTaskEdit(state, task, editor);
|
|
4573
|
-
if (key.name === "escape") return step({ ...state, expandedTaskId: null, taskEditor: null });
|
|
5311
|
+
if (key.name === "escape") return step(settlePlan({ ...state, expandedTaskId: null, taskEditor: null }));
|
|
4574
5312
|
const scroll = scrollDelta(key, state);
|
|
4575
|
-
if (scroll !== null) return scrollPlan(state, scroll);
|
|
4576
|
-
return step({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) });
|
|
5313
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5314
|
+
return step(settlePlan({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) }));
|
|
4577
5315
|
}
|
|
4578
5316
|
function commitTaskEdit(state, task, editor) {
|
|
4579
|
-
const collapsed = { ...state, expandedTaskId: null, taskEditor: null };
|
|
5317
|
+
const collapsed = settlePlan({ ...state, expandedTaskId: null, taskEditor: null });
|
|
4580
5318
|
const prompt = editor.text.trim();
|
|
4581
5319
|
const original = task.prompt ?? task.description ?? task.title;
|
|
4582
5320
|
if (!prompt || prompt === original || !state.sessionId) return step(collapsed);
|
|
@@ -4618,26 +5356,45 @@ function handleApprovalKey(state, overlay, key) {
|
|
|
4618
5356
|
function handleOverlayKey(state, overlay, key) {
|
|
4619
5357
|
if (overlay.kind === "help") return handleHelpKey(state, overlay.scroll ?? 0, key);
|
|
4620
5358
|
if (key.name === "wheelignored") return step(state);
|
|
5359
|
+
if (overlay.kind === "handoff") return handleHandoffKey(state, overlay, key, bodyRows(state));
|
|
4621
5360
|
if (isWheel(key) && overlay.kind !== "picker") return scrollPointed(state, key);
|
|
4622
5361
|
if (overlay.kind === "approval") return handleApprovalKey(state, overlay, key);
|
|
4623
5362
|
if (overlay.kind === "confirm") return handleConfirmKey(state, overlay, key);
|
|
5363
|
+
if (key.name === "escape" && overlay.kind === "picker" && overlay.picker.action.kind === "isolation-blocked") {
|
|
5364
|
+
return chooseBlocked(state, "cancel");
|
|
5365
|
+
}
|
|
4624
5366
|
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
4625
5367
|
if (overlay.kind === "prompt") return handlePromptKey(state, overlay, key);
|
|
4626
5368
|
return handlePickerKey(state, overlay.picker, key);
|
|
4627
5369
|
}
|
|
4628
5370
|
function handleConfirmKey(state, overlay, key) {
|
|
4629
5371
|
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
4630
|
-
|
|
5372
|
+
const { choice } = overlay;
|
|
5373
|
+
if (!choice) return key.name === "enter" ? runConfirm(state, overlay) : step(state);
|
|
5374
|
+
if (key.name === "up" || key.name === "down") {
|
|
5375
|
+
const index = clamp(choice.index + (key.name === "down" ? 1 : -1), choice.options.length - 1);
|
|
5376
|
+
return step({ ...state, overlay: { ...overlay, choice: { ...choice, index } } });
|
|
5377
|
+
}
|
|
5378
|
+
const chosen = key.name === "enter" ? choice.index : key.name === "char" ? Number(key.char) - 1 : -1;
|
|
5379
|
+
const option = choice.options[chosen];
|
|
5380
|
+
if (!option) return step(state);
|
|
5381
|
+
return option.confirms ? runConfirm(state, overlay) : step({ ...state, overlay: null });
|
|
5382
|
+
}
|
|
5383
|
+
function runConfirm(state, overlay) {
|
|
4631
5384
|
const closed = { ...state, overlay: null };
|
|
4632
5385
|
if (overlay.action.kind === "new-session") return newSession(closed);
|
|
4633
5386
|
if (overlay.action.kind === "remove-task") {
|
|
4634
5387
|
if (!state.sessionId) return step(closed);
|
|
4635
5388
|
return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
|
|
4636
5389
|
}
|
|
5390
|
+
if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
|
|
5391
|
+
return confirmedHandoff(state, overlay.action.kind);
|
|
5392
|
+
}
|
|
4637
5393
|
if (overlay.action.kind === "init-workspace") {
|
|
4638
5394
|
return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
|
|
4639
5395
|
}
|
|
4640
|
-
|
|
5396
|
+
const { index } = overlay.action;
|
|
5397
|
+
return withIdlePlanner(closed, (sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index }]));
|
|
4641
5398
|
}
|
|
4642
5399
|
function handleHelpKey(state, scroll, key) {
|
|
4643
5400
|
const page = pageNotch(bodyRows(state));
|
|
@@ -4661,7 +5418,7 @@ function handlePromptKey(state, overlay, key) {
|
|
|
4661
5418
|
if (overlay.action.kind === "api-key") {
|
|
4662
5419
|
return step(closed, [{ type: "setApiKey", provider: overlay.action.provider, key: value2 }]);
|
|
4663
5420
|
}
|
|
4664
|
-
if (!state.sessionId) return
|
|
5421
|
+
if (!state.sessionId) return fail3(closed, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
4665
5422
|
return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
|
|
4666
5423
|
}
|
|
4667
5424
|
const value = editText(overlay.value, key);
|
|
@@ -4694,6 +5451,7 @@ function handlePickerKey(state, picker2, key) {
|
|
|
4694
5451
|
}
|
|
4695
5452
|
function choose(state, picker2, item) {
|
|
4696
5453
|
const closed = { ...state, overlay: null };
|
|
5454
|
+
if (picker2.action.kind === "isolation-blocked") return chooseBlocked(state, item?.id ?? "cancel");
|
|
4697
5455
|
if (picker2.action.kind === "set-allowlist") {
|
|
4698
5456
|
return step(closed, [{ type: "setAllowlist", runner: picker2.action.runner, modelIds: picker2.chosen }]);
|
|
4699
5457
|
}
|
|
@@ -4732,6 +5490,8 @@ function choose(state, picker2, item) {
|
|
|
4732
5490
|
return step(closed, [{ type: "setPlannerEffort", effort: item.id === DEFAULT_EFFORT ? "" : item.id }]);
|
|
4733
5491
|
case "load-session":
|
|
4734
5492
|
return step(closed, [{ type: "loadSession", sessionId: item.id }]);
|
|
5493
|
+
case "rewind":
|
|
5494
|
+
return pickRewindTarget(state, Number(item.id));
|
|
4735
5495
|
case "delete-session":
|
|
4736
5496
|
return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
|
|
4737
5497
|
case "set-key":
|
|
@@ -4801,7 +5561,7 @@ function submit(state) {
|
|
|
4801
5561
|
}
|
|
4802
5562
|
function runCommand(state, { name, args }) {
|
|
4803
5563
|
if (!findCommand(name)) {
|
|
4804
|
-
return
|
|
5564
|
+
return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
|
|
4805
5565
|
}
|
|
4806
5566
|
if (SKILL_IDS.includes(name)) {
|
|
4807
5567
|
return toggleSkill(state, name, args[0]);
|
|
@@ -4812,7 +5572,7 @@ function runCommand(state, { name, args }) {
|
|
|
4812
5572
|
case "quit":
|
|
4813
5573
|
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
4814
5574
|
case "refresh":
|
|
4815
|
-
return step(state, [{ type: "refresh" }]);
|
|
5575
|
+
return step(state, [{ type: "refresh", announce: true }]);
|
|
4816
5576
|
case "run":
|
|
4817
5577
|
return withSession(state, (sessionId) => step(state, [{ type: "execute", sessionId }]));
|
|
4818
5578
|
case "approve":
|
|
@@ -4848,14 +5608,25 @@ function runCommand(state, { name, args }) {
|
|
|
4848
5608
|
{ ...state, overlay: { kind: "picker", picker: picker("Sessions", [], { kind: "load-session" }) } },
|
|
4849
5609
|
[{ type: "loadSessions" }]
|
|
4850
5610
|
);
|
|
5611
|
+
case "handoff":
|
|
5612
|
+
return handoffCommand(state, args[0]);
|
|
5613
|
+
case "fork":
|
|
5614
|
+
return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
|
|
5615
|
+
case "rewind":
|
|
5616
|
+
return rewind(state, args[0]);
|
|
5617
|
+
case "compact":
|
|
5618
|
+
return withIdlePlanner(
|
|
5619
|
+
state,
|
|
5620
|
+
(sessionId) => step({ ...state, status: "planning", busyLabel: "Condensing the conversation\u2026" }, [{ type: "compactConversation", sessionId }])
|
|
5621
|
+
);
|
|
4851
5622
|
case "new":
|
|
4852
5623
|
return requestNewSession(state);
|
|
4853
5624
|
case "save":
|
|
4854
5625
|
return withSession(state, (sessionId) => step(state, [{ type: "saveSession", sessionId }]));
|
|
4855
5626
|
case "load":
|
|
4856
|
-
return args[0] ? step(state, [{ type: "loadSession", sessionId: args[0] }]) :
|
|
5627
|
+
return args[0] ? step(state, [{ type: "loadSession", sessionId: args[0] }]) : fail3(state, "Usage: /load <session-id> \u2014 or run /sessions to pick one.");
|
|
4857
5628
|
case "delete":
|
|
4858
|
-
return args[0] ? step(state, [{ type: "deleteSession", sessionId: args[0] }]) :
|
|
5629
|
+
return args[0] ? step(state, [{ type: "deleteSession", sessionId: args[0] }]) : fail3(state, "Usage: /delete <session-id>");
|
|
4859
5630
|
case "add-task":
|
|
4860
5631
|
return addTask(state, args.join(" "));
|
|
4861
5632
|
case "remove-task":
|
|
@@ -4896,17 +5667,57 @@ function runCommand(state, { name, args }) {
|
|
|
4896
5667
|
(sessionId, taskId) => step(state, [taskActionEffect(state, sessionId, taskId, name)])
|
|
4897
5668
|
);
|
|
4898
5669
|
}
|
|
4899
|
-
return
|
|
5670
|
+
return fail3(state, `/${name} is not wired up yet.`);
|
|
4900
5671
|
}
|
|
4901
5672
|
function picker(title, items, action, extra = {}) {
|
|
4902
5673
|
return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
|
|
4903
5674
|
}
|
|
4904
5675
|
function withSession(state, run) {
|
|
4905
5676
|
if (!state.sessionId) {
|
|
4906
|
-
return
|
|
5677
|
+
return fail3(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
|
|
4907
5678
|
}
|
|
4908
5679
|
return run(state.sessionId);
|
|
4909
5680
|
}
|
|
5681
|
+
function withIdlePlanner(state, run) {
|
|
5682
|
+
return withSession(
|
|
5683
|
+
state,
|
|
5684
|
+
(sessionId) => state.status === "planning" || state.status === "researching" ? fail3(state, "The planner is still answering \u2014 wait for its reply, or /stop it first.") : run(sessionId)
|
|
5685
|
+
);
|
|
5686
|
+
}
|
|
5687
|
+
function rewind(state, arg) {
|
|
5688
|
+
return withIdlePlanner(state, (sessionId) => {
|
|
5689
|
+
if (arg === void 0) {
|
|
5690
|
+
return step(
|
|
5691
|
+
{ ...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." }) } },
|
|
5692
|
+
[{ type: "loadRewindTargets", sessionId }]
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
|
|
5696
|
+
return step(state, [{ type: "loadRewindTargets", sessionId, pick: Number(arg) }]);
|
|
5697
|
+
});
|
|
5698
|
+
}
|
|
5699
|
+
var REWIND_OPTIONS = [
|
|
5700
|
+
{ label: "Restore Conversation", confirms: true },
|
|
5701
|
+
{ label: "Never mind", confirms: false }
|
|
5702
|
+
];
|
|
5703
|
+
function pickRewindTarget(state, index) {
|
|
5704
|
+
return withIdlePlanner({ ...state, overlay: null }, () => {
|
|
5705
|
+
const target = state.rewindTargets?.find((t) => t.index === index);
|
|
5706
|
+
if (!target) return fail3({ ...state, overlay: null }, `No message ${index} to rewind to \u2014 /rewind alone lists them.`);
|
|
5707
|
+
return step({
|
|
5708
|
+
...state,
|
|
5709
|
+
overlay: {
|
|
5710
|
+
kind: "confirm",
|
|
5711
|
+
title: "Rewind",
|
|
5712
|
+
message: "Confirm you want to restore to the point before you sent this message:",
|
|
5713
|
+
quote: target.content,
|
|
5714
|
+
note: "The conversation will be forked.\nThe code will be unchanged.",
|
|
5715
|
+
action: { kind: "rewind", index },
|
|
5716
|
+
choice: { options: REWIND_OPTIONS, index: 0 }
|
|
5717
|
+
}
|
|
5718
|
+
});
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
4910
5721
|
function resolveToggle(arg, current) {
|
|
4911
5722
|
const value = arg?.toLowerCase();
|
|
4912
5723
|
if (value === "on") return true;
|
|
@@ -4916,7 +5727,7 @@ function resolveToggle(arg, current) {
|
|
|
4916
5727
|
}
|
|
4917
5728
|
function toggleSkill(state, skill, arg) {
|
|
4918
5729
|
const enabled = resolveToggle(arg, state.skills[skill]);
|
|
4919
|
-
if (enabled === null) return
|
|
5730
|
+
if (enabled === null) return fail3(state, `Usage: /${skill} [on|off]`);
|
|
4920
5731
|
return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
|
|
4921
5732
|
}
|
|
4922
5733
|
function requestNewSession(state) {
|
|
@@ -4936,6 +5747,7 @@ function newSession(state) {
|
|
|
4936
5747
|
const closeEffects = state.sessionId ? [{ type: "closeSession", sessionId: state.sessionId }] : [];
|
|
4937
5748
|
return step({
|
|
4938
5749
|
...state,
|
|
5750
|
+
handoff: null,
|
|
4939
5751
|
sessionId: null,
|
|
4940
5752
|
goal: "",
|
|
4941
5753
|
tasks: [],
|
|
@@ -4945,6 +5757,7 @@ function newSession(state) {
|
|
|
4945
5757
|
expandedTaskId: null,
|
|
4946
5758
|
taskEditor: null,
|
|
4947
5759
|
scroll: 0,
|
|
5760
|
+
planScroll: null,
|
|
4948
5761
|
status: "idle",
|
|
4949
5762
|
busyLabel: "",
|
|
4950
5763
|
thinkingLine: "",
|
|
@@ -4956,14 +5769,14 @@ function newSession(state) {
|
|
|
4956
5769
|
}
|
|
4957
5770
|
function setModel(state, args) {
|
|
4958
5771
|
if (args[0] === "set" && args[1]) {
|
|
4959
|
-
const runner = (0,
|
|
5772
|
+
const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
|
|
4960
5773
|
const known = runner ? modelsForRunner(state.models, runner) : [];
|
|
4961
5774
|
if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
|
|
4962
|
-
return
|
|
5775
|
+
return fail3(state, `${args[1]} was not discovered for ${runner}.`);
|
|
4963
5776
|
}
|
|
4964
5777
|
return step(state, [{ type: "setModel", modelId: args[1] }]);
|
|
4965
5778
|
}
|
|
4966
|
-
if (args.length > 0 && args[0] !== "set") return
|
|
5779
|
+
if (args.length > 0 && args[0] !== "set") return fail3(state, "Usage: /model [set <model-id>]");
|
|
4967
5780
|
const items = pickerItemsFor(state, { kind: "set-model" });
|
|
4968
5781
|
return step(
|
|
4969
5782
|
{
|
|
@@ -4978,21 +5791,21 @@ function setModel(state, args) {
|
|
|
4978
5791
|
}
|
|
4979
5792
|
function plannerItems(state) {
|
|
4980
5793
|
const installed = new Set(state.runners.map((r) => r.id));
|
|
4981
|
-
const agents =
|
|
4982
|
-
const runner = (0,
|
|
5794
|
+
const agents = import_core18.CLI_PROVIDERS.map((id) => {
|
|
5795
|
+
const runner = (0, import_core18.runnerForProvider)(id);
|
|
4983
5796
|
const ready = installed.has(runner);
|
|
4984
5797
|
return {
|
|
4985
5798
|
id,
|
|
4986
|
-
label:
|
|
5799
|
+
label: import_core18.ALL_PROVIDERS[id].label,
|
|
4987
5800
|
detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
|
|
4988
5801
|
selected: state.plannerProvider === id,
|
|
4989
5802
|
disabled: !ready
|
|
4990
5803
|
};
|
|
4991
5804
|
});
|
|
4992
|
-
const vendors =
|
|
5805
|
+
const vendors = import_core18.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
|
|
4993
5806
|
id,
|
|
4994
|
-
label:
|
|
4995
|
-
detail:
|
|
5807
|
+
label: import_core18.ALL_PROVIDERS[id].label,
|
|
5808
|
+
detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
4996
5809
|
selected: state.plannerProvider === id
|
|
4997
5810
|
}));
|
|
4998
5811
|
return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
|
|
@@ -5001,7 +5814,7 @@ function setPlanner(state, args) {
|
|
|
5001
5814
|
if (args[0]) {
|
|
5002
5815
|
const id = args[0].toLowerCase();
|
|
5003
5816
|
if (!KNOWN_PROVIDERS2.includes(id)) {
|
|
5004
|
-
return
|
|
5817
|
+
return fail3(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
|
|
5005
5818
|
}
|
|
5006
5819
|
return step(state, [{ type: "setPlanner", provider: id }]);
|
|
5007
5820
|
}
|
|
@@ -5022,7 +5835,7 @@ function setPlannerEffort(state, args) {
|
|
|
5022
5835
|
const match = items.find((i) => !i.disabled && (i.id === wanted || wanted === "default" && i.id === DEFAULT_EFFORT));
|
|
5023
5836
|
if (!match) {
|
|
5024
5837
|
const available = items.filter((i) => !i.disabled && i.id !== DEFAULT_EFFORT).map((i) => i.id);
|
|
5025
|
-
return
|
|
5838
|
+
return fail3(state, available.length > 0 ? `Unknown effort: ${args[0]}. Available: ${available.join(", ")}, default.` : items[0]?.label ?? "No effort levels available.");
|
|
5026
5839
|
}
|
|
5027
5840
|
return step(state, [{ type: "setPlannerEffort", effort: match.id === DEFAULT_EFFORT ? "" : match.id }]);
|
|
5028
5841
|
}
|
|
@@ -5040,7 +5853,7 @@ function setKey(state, args) {
|
|
|
5040
5853
|
if (args[0] === "set" && args[1]) {
|
|
5041
5854
|
const provider = args[1].toLowerCase();
|
|
5042
5855
|
if (!KNOWN_PROVIDERS2.includes(provider)) {
|
|
5043
|
-
return
|
|
5856
|
+
return fail3(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
|
|
5044
5857
|
}
|
|
5045
5858
|
if (!args[2]) {
|
|
5046
5859
|
return step({
|
|
@@ -5050,16 +5863,16 @@ function setKey(state, args) {
|
|
|
5050
5863
|
}
|
|
5051
5864
|
return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
|
|
5052
5865
|
}
|
|
5053
|
-
const items =
|
|
5866
|
+
const items = import_core18.PROVIDER_PRIORITY.map((id) => ({
|
|
5054
5867
|
id,
|
|
5055
|
-
label:
|
|
5056
|
-
detail:
|
|
5868
|
+
label: import_core18.ALL_PROVIDERS[id].label,
|
|
5869
|
+
detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5057
5870
|
selected: state.configuredProviders.includes(id)
|
|
5058
5871
|
}));
|
|
5059
5872
|
return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
|
|
5060
5873
|
}
|
|
5061
5874
|
function keyPrompt(provider) {
|
|
5062
|
-
const meta =
|
|
5875
|
+
const meta = import_core18.ALL_PROVIDERS[provider];
|
|
5063
5876
|
return {
|
|
5064
5877
|
kind: "prompt",
|
|
5065
5878
|
title: `${meta.label} API key`,
|
|
@@ -5071,23 +5884,23 @@ function keyPrompt(provider) {
|
|
|
5071
5884
|
function allowlist(state, args) {
|
|
5072
5885
|
const [sub, runner, ...rest] = args;
|
|
5073
5886
|
if (sub === "set") {
|
|
5074
|
-
if (!runner || rest.length === 0) return
|
|
5887
|
+
if (!runner || rest.length === 0) return fail3(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
|
|
5075
5888
|
if (state.runners.length > 0 && !state.runners.some((r) => r.id === runner)) {
|
|
5076
|
-
return
|
|
5889
|
+
return fail3(state, `Unknown runner "${runner}".`);
|
|
5077
5890
|
}
|
|
5078
5891
|
const modelIds = rest.join(" ").split(",").map((s) => s.trim()).filter(Boolean);
|
|
5079
|
-
if (modelIds.length === 0) return
|
|
5892
|
+
if (modelIds.length === 0) return fail3(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
|
|
5080
5893
|
const known = modelsForRunner(state.models, runner);
|
|
5081
5894
|
if (state.models.length > 0 && known.length > 0) {
|
|
5082
5895
|
const stray = modelIds.filter((id) => !known.some((m) => m.id === id));
|
|
5083
5896
|
if (stray.length > 0) {
|
|
5084
|
-
return
|
|
5897
|
+
return fail3(state, `Not discovered for ${runner}: ${stray.join(", ")}.`);
|
|
5085
5898
|
}
|
|
5086
5899
|
}
|
|
5087
5900
|
return step(state, [{ type: "setAllowlist", runner, modelIds }]);
|
|
5088
5901
|
}
|
|
5089
5902
|
if (sub === "clear") {
|
|
5090
|
-
if (!runner) return
|
|
5903
|
+
if (!runner) return fail3(state, "Usage: /allowlist clear <runner>");
|
|
5091
5904
|
return step(state, [{ type: "setAllowlist", runner, modelIds: [] }]);
|
|
5092
5905
|
}
|
|
5093
5906
|
if (sub === "show" || sub === void 0) {
|
|
@@ -5109,7 +5922,7 @@ function allowlist(state, args) {
|
|
|
5109
5922
|
[{ type: "loadModels" }]
|
|
5110
5923
|
);
|
|
5111
5924
|
}
|
|
5112
|
-
return
|
|
5925
|
+
return fail3(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
|
|
5113
5926
|
}
|
|
5114
5927
|
function describeAllowlist(ids) {
|
|
5115
5928
|
if (!ids || ids.length === 0) return "no restriction";
|
|
@@ -5120,7 +5933,7 @@ function runners(state, args) {
|
|
|
5120
5933
|
if (runner) {
|
|
5121
5934
|
const known = state.runners.find((r) => r.id === runner);
|
|
5122
5935
|
const enabled = resolveToggle(arg, known?.enabled ?? true);
|
|
5123
|
-
if (enabled === null) return
|
|
5936
|
+
if (enabled === null) return fail3(state, "Usage: /runners [<runner-id> on|off]");
|
|
5124
5937
|
return step(state, [{ type: "setRunnerEnabled", runner, enabled }]);
|
|
5125
5938
|
}
|
|
5126
5939
|
const action = { kind: "set-runners" };
|
|
@@ -5136,12 +5949,12 @@ function runners(state, args) {
|
|
|
5136
5949
|
}
|
|
5137
5950
|
function setAutonomous(state, arg) {
|
|
5138
5951
|
const enabled = resolveToggle(arg, state.autonomous);
|
|
5139
|
-
if (enabled === null) return
|
|
5952
|
+
if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
|
|
5140
5953
|
return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
|
|
5141
5954
|
}
|
|
5142
5955
|
function setMouseCapture(state, arg) {
|
|
5143
5956
|
const enabled = resolveToggle(arg, state.mouseCapture);
|
|
5144
|
-
if (enabled === null) return
|
|
5957
|
+
if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
|
|
5145
5958
|
const noted = say(
|
|
5146
5959
|
// A selection made under capture is meaningless once the terminal owns the
|
|
5147
5960
|
// mouse again, and its highlight would sit on screen with nothing able to
|
|
@@ -5153,7 +5966,7 @@ function setMouseCapture(state, arg) {
|
|
|
5153
5966
|
return step(noted, [{ type: "setMouseCapture", enabled }]);
|
|
5154
5967
|
}
|
|
5155
5968
|
function openTaskRunnerPicker(state, task) {
|
|
5156
|
-
if (task.type !== "ai") return
|
|
5969
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
|
|
5157
5970
|
const action = { kind: "set-task-runner", taskId: task.id };
|
|
5158
5971
|
return step(
|
|
5159
5972
|
{
|
|
@@ -5183,7 +5996,7 @@ function confirmRemoveTask(state, task) {
|
|
|
5183
5996
|
function openTaskDepsPicker(state, task) {
|
|
5184
5997
|
const action = { kind: "set-task-deps", taskId: task.id };
|
|
5185
5998
|
const items = pickerItemsFor(state, action);
|
|
5186
|
-
if (items.length === 0) return
|
|
5999
|
+
if (items.length === 0) return fail3(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
|
|
5187
6000
|
return step({
|
|
5188
6001
|
...state,
|
|
5189
6002
|
overlay: {
|
|
@@ -5197,10 +6010,10 @@ function openTaskDepsPicker(state, task) {
|
|
|
5197
6010
|
});
|
|
5198
6011
|
}
|
|
5199
6012
|
function openTaskModePicker(state, task) {
|
|
5200
|
-
if (task.type !== "ai") return
|
|
6013
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor mode.");
|
|
5201
6014
|
const modes = modesForTask(state.modesByRunner, task);
|
|
5202
6015
|
if (modes.length === 0) {
|
|
5203
|
-
return
|
|
6016
|
+
return fail3(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
|
|
5204
6017
|
}
|
|
5205
6018
|
const action = { kind: "set-task-mode", taskId: task.id };
|
|
5206
6019
|
return step(
|
|
@@ -5216,7 +6029,7 @@ function openTaskModePicker(state, task) {
|
|
|
5216
6029
|
);
|
|
5217
6030
|
}
|
|
5218
6031
|
function openTaskModelPicker(state, task) {
|
|
5219
|
-
if (task.type !== "ai") return
|
|
6032
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor model.");
|
|
5220
6033
|
const action = { kind: "set-task-model", taskId: task.id };
|
|
5221
6034
|
return step(
|
|
5222
6035
|
{
|
|
@@ -5232,8 +6045,8 @@ function openTaskModelPicker(state, task) {
|
|
|
5232
6045
|
);
|
|
5233
6046
|
}
|
|
5234
6047
|
function openTaskEffortPicker(state, task) {
|
|
5235
|
-
if (task.type !== "ai") return
|
|
5236
|
-
if (!task.assignedModel) return
|
|
6048
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have a thinking effort.");
|
|
6049
|
+
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
5237
6050
|
const action = { kind: "set-task-effort", taskId: task.id };
|
|
5238
6051
|
return step(
|
|
5239
6052
|
{
|
|
@@ -5299,7 +6112,7 @@ function taskModelCommand(state, args) {
|
|
|
5299
6112
|
variants: []
|
|
5300
6113
|
};
|
|
5301
6114
|
if (!runnerAccepts(task, model)) {
|
|
5302
|
-
return
|
|
6115
|
+
return fail3(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
|
|
5303
6116
|
}
|
|
5304
6117
|
return assignTaskModel(state, sessionId, task, model);
|
|
5305
6118
|
});
|
|
@@ -5309,7 +6122,7 @@ function taskRunnerCommand(state, args) {
|
|
|
5309
6122
|
const task = findTask(state.tasks, taskId);
|
|
5310
6123
|
if (!args[1]) return openTaskRunnerPicker(state, task);
|
|
5311
6124
|
const runner = state.runners.find((candidate) => candidate.id === args[1]);
|
|
5312
|
-
if (!runner) return
|
|
6125
|
+
if (!runner) return fail3(state, `Unknown runner "${args[1]}".`);
|
|
5313
6126
|
return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
|
|
5314
6127
|
});
|
|
5315
6128
|
}
|
|
@@ -5318,7 +6131,7 @@ function taskModeCommand(state, args) {
|
|
|
5318
6131
|
const task = findTask(state.tasks, taskId);
|
|
5319
6132
|
if (!args[1]) return openTaskModePicker(state, task);
|
|
5320
6133
|
const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
|
|
5321
|
-
if (!mode) return
|
|
6134
|
+
if (!mode) return fail3(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
|
|
5322
6135
|
return assignTaskMode(state, sessionId, task, mode.id, mode.label);
|
|
5323
6136
|
});
|
|
5324
6137
|
}
|
|
@@ -5326,32 +6139,36 @@ function taskEffortCommand(state, args) {
|
|
|
5326
6139
|
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
5327
6140
|
const task = findTask(state.tasks, taskId);
|
|
5328
6141
|
if (!args[1]) return openTaskEffortPicker(state, task);
|
|
5329
|
-
if (!task.assignedModel) return
|
|
6142
|
+
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
5330
6143
|
const effort = args[1].toLowerCase();
|
|
5331
6144
|
const value = effort === "default" || effort === "none" ? void 0 : args[1];
|
|
5332
6145
|
const supported = effortsForTask(state.models, task);
|
|
5333
6146
|
if (value && supported.length > 0 && !supported.some((variant) => variant.id === value)) {
|
|
5334
|
-
return
|
|
6147
|
+
return fail3(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
|
|
5335
6148
|
}
|
|
5336
6149
|
return assignTaskEffort(state, sessionId, task, value);
|
|
5337
6150
|
});
|
|
5338
6151
|
}
|
|
6152
|
+
function clampSelection(index, rows) {
|
|
6153
|
+
return Math.max(0, Math.min(index, rows - 1));
|
|
6154
|
+
}
|
|
5339
6155
|
function addTask(state, title) {
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
}
|
|
6156
|
+
if (!state.sessionId) {
|
|
6157
|
+
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
6158
|
+
}
|
|
6159
|
+
if (!title) {
|
|
6160
|
+
return step({
|
|
6161
|
+
...state,
|
|
6162
|
+
overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
|
|
6163
|
+
});
|
|
6164
|
+
}
|
|
6165
|
+
return step(state, [{ type: "addTask", sessionId: state.sessionId, title }]);
|
|
5349
6166
|
}
|
|
5350
6167
|
function taskCommand(state, token, run) {
|
|
5351
6168
|
return withSession(state, (sessionId) => {
|
|
5352
|
-
if (!token) return
|
|
6169
|
+
if (!token) return fail3(state, "Which task? Pass a task id or its number in the plan.");
|
|
5353
6170
|
const taskId = resolveTaskId2(state, token);
|
|
5354
|
-
if (!taskId) return
|
|
6171
|
+
if (!taskId) return fail3(state, `No task matching "${token}" in the current plan.`);
|
|
5355
6172
|
return run(sessionId, taskId);
|
|
5356
6173
|
});
|
|
5357
6174
|
}
|
|
@@ -5417,7 +6234,7 @@ function createApp(deps) {
|
|
|
5417
6234
|
|
|
5418
6235
|
// src/tui/effects.ts
|
|
5419
6236
|
var import_child_process4 = require("child_process");
|
|
5420
|
-
var
|
|
6237
|
+
var import_core19 = require("@ordewell/core");
|
|
5421
6238
|
var import_plan_utils4 = require("@ordewell/core/plan-utils");
|
|
5422
6239
|
var ConversationQueue = class {
|
|
5423
6240
|
tail = Promise.resolve();
|
|
@@ -5588,7 +6405,7 @@ async function perform(effect, deps) {
|
|
|
5588
6405
|
dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
|
|
5589
6406
|
return;
|
|
5590
6407
|
case "setPlanner": {
|
|
5591
|
-
const meta =
|
|
6408
|
+
const meta = import_core19.ALL_PROVIDERS[effect.provider];
|
|
5592
6409
|
if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
|
|
5593
6410
|
const env = { AI_PROVIDER: effect.provider };
|
|
5594
6411
|
const settings = await api.updateSettings({ env });
|
|
@@ -5600,7 +6417,7 @@ async function perform(effect, deps) {
|
|
|
5600
6417
|
});
|
|
5601
6418
|
dispatch({
|
|
5602
6419
|
type: "notice",
|
|
5603
|
-
message: `${(0,
|
|
6420
|
+
message: `${(0, import_core19.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
|
|
5604
6421
|
});
|
|
5605
6422
|
await loadModels(deps);
|
|
5606
6423
|
return;
|
|
@@ -5612,7 +6429,7 @@ async function perform(effect, deps) {
|
|
|
5612
6429
|
dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
|
|
5613
6430
|
return;
|
|
5614
6431
|
case "setApiKey": {
|
|
5615
|
-
const meta =
|
|
6432
|
+
const meta = import_core19.ALL_PROVIDERS[effect.provider];
|
|
5616
6433
|
if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
|
|
5617
6434
|
await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
|
|
5618
6435
|
deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
|
|
@@ -5668,6 +6485,79 @@ async function perform(effect, deps) {
|
|
|
5668
6485
|
dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
|
|
5669
6486
|
return;
|
|
5670
6487
|
}
|
|
6488
|
+
// The daemon has already adopted the fork, so switching is only the TUI
|
|
6489
|
+
// catching up — the original session is not closed: it may be running.
|
|
6490
|
+
case "forkConversation": {
|
|
6491
|
+
const fork = await api.forkConversation(effect.sessionId);
|
|
6492
|
+
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6493
|
+
dispatch({ type: "chatRestored", history: fork.plan.conversationHistory ?? [], sessionId: fork.sessionId });
|
|
6494
|
+
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6495
|
+
dispatch({ type: "notice", message: `Forked ${effect.sessionId} into ${fork.sessionId} \u2014 you are in the fork now. /sessions to go back.` });
|
|
6496
|
+
return;
|
|
6497
|
+
}
|
|
6498
|
+
case "loadRewindTargets":
|
|
6499
|
+
dispatch({
|
|
6500
|
+
type: "rewindTargetsLoaded",
|
|
6501
|
+
targets: await api.rewindTargets(effect.sessionId),
|
|
6502
|
+
sessionId: effect.sessionId,
|
|
6503
|
+
...effect.pick !== void 0 && { pick: effect.pick }
|
|
6504
|
+
});
|
|
6505
|
+
return;
|
|
6506
|
+
// A rewind is a fork from an earlier point, so the TUI follows it the way
|
|
6507
|
+
// it follows `/fork`; the original keeps its whole conversation. The
|
|
6508
|
+
// rewound message comes back into the input, to edit and send again.
|
|
6509
|
+
case "rewindConversation": {
|
|
6510
|
+
const fork = await api.rewindConversation(effect.sessionId, effect.index);
|
|
6511
|
+
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6512
|
+
dispatch({ type: "chatRestored", history: fork.plan.conversationHistory ?? [], sessionId: fork.sessionId });
|
|
6513
|
+
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6514
|
+
dispatch({ type: "inputPrefilled", text: fork.rewoundMessage, sessionId: fork.sessionId });
|
|
6515
|
+
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.` });
|
|
6516
|
+
return;
|
|
6517
|
+
}
|
|
6518
|
+
// The redrawn transcript opens with the summary entry, which is what the
|
|
6519
|
+
// user gets to read — no separate notice to repeat it.
|
|
6520
|
+
case "compactConversation": {
|
|
6521
|
+
const { plan } = await api.compactConversation(effect.sessionId);
|
|
6522
|
+
dispatch({ type: "chatRestored", history: plan.conversationHistory ?? [], sessionId: effect.sessionId });
|
|
6523
|
+
dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
|
|
6524
|
+
return;
|
|
6525
|
+
}
|
|
6526
|
+
case "isolationReviewDiff":
|
|
6527
|
+
dispatch({ type: "handoffDiff", diff: await api.reviewRunDiff(effect.sessionId), sessionId: effect.sessionId });
|
|
6528
|
+
return;
|
|
6529
|
+
// A conflict or a refusal is an answer, not a fault: either way the user's
|
|
6530
|
+
// tree is exactly as it was, and the words say what to do next. A full
|
|
6531
|
+
// merge leaves nothing to hand over — the daemon has cleared the run up.
|
|
6532
|
+
case "isolationMerge": {
|
|
6533
|
+
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
|
|
6534
|
+
if (ok) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
6535
|
+
dispatch({ type: ok ? "notice" : "failed", message });
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6538
|
+
case "isolationDiscard":
|
|
6539
|
+
await api.discardRun(effect.sessionId);
|
|
6540
|
+
dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
6541
|
+
dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
|
|
6542
|
+
return;
|
|
6543
|
+
case "isolationCleanup":
|
|
6544
|
+
await api.cleanupRun(effect.sessionId);
|
|
6545
|
+
await refreshPlan(deps, effect.sessionId);
|
|
6546
|
+
dispatch({ type: "notice", message: `Removed the run's worktrees and task branches; ${effect.branch} is kept.` });
|
|
6547
|
+
return;
|
|
6548
|
+
// Spawns runners again, so it needs the stream a run needs — the blocked
|
|
6549
|
+
// one closed itself when nothing started.
|
|
6550
|
+
case "isolationContinue": {
|
|
6551
|
+
const { sessionId } = effect;
|
|
6552
|
+
await withExecutionStream(deps, sessionId, () => effect.mode === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
|
|
6553
|
+
await refreshPlan(deps, sessionId);
|
|
6554
|
+
return;
|
|
6555
|
+
}
|
|
6556
|
+
case "resolveConflict":
|
|
6557
|
+
await api.resolveConflictAsTask(effect.sessionId, effect.taskId);
|
|
6558
|
+
await refreshPlan(deps, effect.sessionId);
|
|
6559
|
+
dispatch({ type: "notice", message: "Added a task that merges the conflicted branch by hand. Run it with E (run plan); when it lands, the conflicted task lands through it." });
|
|
6560
|
+
return;
|
|
5671
6561
|
case "deleteSession":
|
|
5672
6562
|
await api.deleteSession(effect.sessionId, workspace);
|
|
5673
6563
|
await loadSessions(deps);
|
|
@@ -5684,7 +6574,7 @@ async function perform(effect, deps) {
|
|
|
5684
6574
|
return;
|
|
5685
6575
|
case "refresh":
|
|
5686
6576
|
await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
|
|
5687
|
-
dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
6577
|
+
if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
5688
6578
|
return;
|
|
5689
6579
|
case "exit":
|
|
5690
6580
|
deps.exit();
|
|
@@ -5700,7 +6590,7 @@ function osc52(text) {
|
|
|
5700
6590
|
function copySelection(deps, text) {
|
|
5701
6591
|
if (!text) return;
|
|
5702
6592
|
const lines = text.split("\n").length;
|
|
5703
|
-
const command = (0,
|
|
6593
|
+
const command = (0, import_core19.clipboardCopyCommand)(deps.hasBin);
|
|
5704
6594
|
if (command) {
|
|
5705
6595
|
try {
|
|
5706
6596
|
(deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
|
|
@@ -5831,7 +6721,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
5831
6721
|
case "status_update": {
|
|
5832
6722
|
const updates = {};
|
|
5833
6723
|
for (const task of event.tasks ?? []) {
|
|
5834
|
-
updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null };
|
|
6724
|
+
updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
|
|
5835
6725
|
}
|
|
5836
6726
|
dispatch({ type: "tasksStatus", updates, sessionId });
|
|
5837
6727
|
return;
|
|
@@ -5842,7 +6732,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
5842
6732
|
// The extension shows these in its checkpoint panel; here they are
|
|
5843
6733
|
// transcript lines, which is the TUI's equivalent surface.
|
|
5844
6734
|
case "checkpoint":
|
|
5845
|
-
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0,
|
|
6735
|
+
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core19.truncateCheckpointSummary)(event.summary)}` });
|
|
5846
6736
|
return;
|
|
5847
6737
|
case "approval_decided":
|
|
5848
6738
|
dispatch({ type: "notice", message: describeApprovalDecision2(event) });
|
|
@@ -5873,10 +6763,41 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
5873
6763
|
case "execution_stopped":
|
|
5874
6764
|
dispatch({ type: "executionComplete", stopped: true, sessionId });
|
|
5875
6765
|
return;
|
|
5876
|
-
//
|
|
5877
|
-
|
|
5878
|
-
|
|
6766
|
+
// Nothing started; the user chooses how to go on.
|
|
6767
|
+
case "isolation_blocked":
|
|
6768
|
+
dispatch({ type: "isolationBlocked", message: event.message, ...event.repos ? { repos: event.repos } : {}, sessionId });
|
|
6769
|
+
return;
|
|
6770
|
+
// How the run isolates. The daemon has no toast channel, so this is the only place the user hears it.
|
|
6771
|
+
case "notice":
|
|
6772
|
+
dispatch({ type: "notice", message: event.message });
|
|
6773
|
+
return;
|
|
6774
|
+
case "isolation_handoff":
|
|
6775
|
+
dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
|
|
5879
6776
|
return;
|
|
6777
|
+
// Raw runner chatter, and the planner's own turn: the status line and the
|
|
6778
|
+
// plan pane already say everything the user needs during a run. Listed
|
|
6779
|
+
// rather than defaulted, so a new SessionMessage variant fails to compile
|
|
6780
|
+
// here until someone decides what a run's watcher does with it.
|
|
6781
|
+
case "task_updated":
|
|
6782
|
+
case "task_output":
|
|
6783
|
+
case "plan_thinking":
|
|
6784
|
+
case "planner_liveness":
|
|
6785
|
+
case "research_step":
|
|
6786
|
+
case "plan_token":
|
|
6787
|
+
case "research_step_done":
|
|
6788
|
+
case "approval_request":
|
|
6789
|
+
case "approval_settled":
|
|
6790
|
+
return;
|
|
6791
|
+
// Its asker already has the words, from the merge request itself; a
|
|
6792
|
+
// viewer that did not ask still has to drop a run that is gone.
|
|
6793
|
+
case "isolation_merge":
|
|
6794
|
+
if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
|
|
6795
|
+
return;
|
|
6796
|
+
default: {
|
|
6797
|
+
const unhandled = event;
|
|
6798
|
+
void unhandled;
|
|
6799
|
+
return;
|
|
6800
|
+
}
|
|
5880
6801
|
}
|
|
5881
6802
|
}
|
|
5882
6803
|
async function refreshPlan(deps, sessionId) {
|
|
@@ -6195,7 +7116,7 @@ async function handleTui(subArgs) {
|
|
|
6195
7116
|
}
|
|
6196
7117
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
6197
7118
|
try {
|
|
6198
|
-
(0,
|
|
7119
|
+
(0, import_core20.assertWorkspaceExists)(workspace);
|
|
6199
7120
|
} catch {
|
|
6200
7121
|
console.error(
|
|
6201
7122
|
`Workspace "${workspace}" does not exist.
|
|
@@ -6204,7 +7125,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6204
7125
|
);
|
|
6205
7126
|
process.exit(1);
|
|
6206
7127
|
}
|
|
6207
|
-
registerSkillCommands((0,
|
|
7128
|
+
registerSkillCommands((0, import_core20.createSkillsService)(workspace).listSkills());
|
|
6208
7129
|
const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
|
|
6209
7130
|
const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
|
|
6210
7131
|
const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
|
|
@@ -6223,7 +7144,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6223
7144
|
conversationQueue,
|
|
6224
7145
|
port,
|
|
6225
7146
|
dispatch: (action) => app.dispatch(action),
|
|
6226
|
-
newSessionId: () => (0,
|
|
7147
|
+
newSessionId: () => (0, import_core20.mintSessionId)(),
|
|
6227
7148
|
setEnvVar: (key, value) => {
|
|
6228
7149
|
writeEnvVar(findEnvFile(), key, value);
|
|
6229
7150
|
process.env[key] = value;
|
|
@@ -6311,6 +7232,10 @@ var COMMANDS = {
|
|
|
6311
7232
|
"task-deps": handleTaskDeps,
|
|
6312
7233
|
terminal: handleTerminal,
|
|
6313
7234
|
"sessions": handleSessions,
|
|
7235
|
+
fork: handleFork,
|
|
7236
|
+
compact: handleCompact,
|
|
7237
|
+
rewind: handleRewind,
|
|
7238
|
+
handoff: handleHandoff,
|
|
6314
7239
|
tui: handleTui
|
|
6315
7240
|
};
|
|
6316
7241
|
|
|
@@ -6329,7 +7254,7 @@ function cliVersion() {
|
|
|
6329
7254
|
|
|
6330
7255
|
// src/main.ts
|
|
6331
7256
|
async function main() {
|
|
6332
|
-
(0,
|
|
7257
|
+
(0, import_core21.migrateOldConfigDir)();
|
|
6333
7258
|
loadEnvFile();
|
|
6334
7259
|
const argv = process.argv.slice(2);
|
|
6335
7260
|
const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");
|