@ordewell/cli 0.4.23 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +1204 -437
- 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_core20 = 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>] Rewind the conversation to 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,83 @@ 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
|
+
/** Cut the conversation back to just before the user message at `index` (a transcript position). */
|
|
620
|
+
async rewindConversation(sessionId, index) {
|
|
621
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
|
|
622
|
+
if (res.status !== 200) {
|
|
623
|
+
throw new Error(res.data?.error || "Rewind failed");
|
|
624
|
+
}
|
|
625
|
+
return res.data.plan;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Condense the conversation into a summary. One planner call, so it can take
|
|
629
|
+
* as long as a reply; a refusal or failure leaves the conversation as it was.
|
|
630
|
+
*/
|
|
631
|
+
async compactConversation(sessionId) {
|
|
632
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/compact`);
|
|
633
|
+
if (res.status !== 200) {
|
|
634
|
+
throw new Error(res.data?.error || "Compaction failed");
|
|
635
|
+
}
|
|
636
|
+
return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
|
|
637
|
+
}
|
|
638
|
+
async reviewRunDiff(sessionId) {
|
|
639
|
+
const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
|
|
640
|
+
if (res.status !== 200) {
|
|
641
|
+
throw new Error(res.data?.error || "Could not read the diff");
|
|
642
|
+
}
|
|
643
|
+
return res.data.diff;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* A conflict or a block is an outcome, not an error. Passed on whole: which
|
|
647
|
+
* repos blocked the merge, or landed before it stopped, is the answer.
|
|
648
|
+
*/
|
|
649
|
+
async mergeRun(sessionId) {
|
|
650
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/merge`);
|
|
651
|
+
if (res.status !== 200) {
|
|
652
|
+
throw new Error(res.data?.error || "Merge failed");
|
|
653
|
+
}
|
|
654
|
+
return res.data;
|
|
655
|
+
}
|
|
656
|
+
discardRun(sessionId) {
|
|
657
|
+
return this.isolationAction(sessionId, "discard", "Discard failed");
|
|
658
|
+
}
|
|
659
|
+
cleanupRun(sessionId) {
|
|
660
|
+
return this.isolationAction(sessionId, "cleanup", "Cleanup failed");
|
|
661
|
+
}
|
|
662
|
+
continueWithStash(sessionId) {
|
|
663
|
+
return this.isolationAction(sessionId, "stash-and-continue", "Could not stash and continue");
|
|
664
|
+
}
|
|
665
|
+
continueWithoutIsolation(sessionId) {
|
|
666
|
+
return this.isolationAction(sessionId, "run-without", "Could not continue without isolation");
|
|
667
|
+
}
|
|
668
|
+
async isolationAction(sessionId, segment, failure) {
|
|
669
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/${segment}`);
|
|
670
|
+
if (res.status !== 200) {
|
|
671
|
+
throw new Error(res.data?.error || failure);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
async resolveConflictAsTask(sessionId, taskId) {
|
|
675
|
+
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`);
|
|
676
|
+
if (res.status !== 200) {
|
|
677
|
+
throw new Error(res.data?.error || "Could not add a resolver task");
|
|
678
|
+
}
|
|
679
|
+
return res.data.plan;
|
|
680
|
+
}
|
|
601
681
|
async closeSession(sessionId) {
|
|
602
682
|
const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/close`);
|
|
603
683
|
return res.data;
|
|
@@ -676,7 +756,7 @@ var ApiClient = class {
|
|
|
676
756
|
try {
|
|
677
757
|
const event = JSON.parse(data.toString());
|
|
678
758
|
onEvent(event);
|
|
679
|
-
if ((event.type === "execution_complete" || event.type === "execution_stopped") && !resolved) {
|
|
759
|
+
if ((event.type === "execution_complete" || event.type === "execution_stopped" || event.type === "isolation_blocked") && !resolved) {
|
|
680
760
|
resolved = true;
|
|
681
761
|
socket.close();
|
|
682
762
|
resolve3();
|
|
@@ -1176,6 +1256,96 @@ ${lastPlannerMessage(plan)}
|
|
|
1176
1256
|
}
|
|
1177
1257
|
}
|
|
1178
1258
|
|
|
1259
|
+
// src/isolation.ts
|
|
1260
|
+
var import_core7 = require("@ordewell/core");
|
|
1261
|
+
var STATE_OF = {
|
|
1262
|
+
active: "active",
|
|
1263
|
+
merged: "integrated",
|
|
1264
|
+
conflict: "conflict",
|
|
1265
|
+
// A failed integration keeps its refs exactly as a failed verdict does; the
|
|
1266
|
+
// user sees one thing: work that did not land and can be looked at.
|
|
1267
|
+
kept: "kept",
|
|
1268
|
+
failed: "kept"
|
|
1269
|
+
};
|
|
1270
|
+
function reposOf(run) {
|
|
1271
|
+
if (Array.isArray(run.repos)) {
|
|
1272
|
+
const repos = run.repos.filter((r) => r && typeof r.path === "string" && typeof r.integrationBranch === "string" && typeof r.baseRef === "string");
|
|
1273
|
+
return repos.length > 0 ? repos.map((r) => ({ path: String(r.path), integrationBranch: String(r.integrationBranch), baseRef: String(r.baseRef) })) : null;
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
|
|
1276
|
+
return [{ path: ".", integrationBranch: run.integrationBranch, baseRef: run.baseRef }];
|
|
1277
|
+
}
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
function changedRepos(record, legacy, state) {
|
|
1281
|
+
if (legacy) return state === "integrated" ? ["."] : [];
|
|
1282
|
+
return Object.entries(record.repos ?? {}).filter(([, r]) => r?.changed === true).map(([path]) => path);
|
|
1283
|
+
}
|
|
1284
|
+
function isolationOfPlan(plan) {
|
|
1285
|
+
const run = plan?.isolation?.run;
|
|
1286
|
+
const repos = run ? reposOf(run) : null;
|
|
1287
|
+
if (!run || !repos) return null;
|
|
1288
|
+
const legacy = !Array.isArray(run.repos);
|
|
1289
|
+
const records = Object.entries(run.tasks ?? {});
|
|
1290
|
+
const tasks = {};
|
|
1291
|
+
const landed = [];
|
|
1292
|
+
for (const [taskId, record] of records) {
|
|
1293
|
+
const state = STATE_OF[String(record.status)];
|
|
1294
|
+
if (!state) continue;
|
|
1295
|
+
const changed = changedRepos(record, legacy, state);
|
|
1296
|
+
tasks[taskId] = {
|
|
1297
|
+
state,
|
|
1298
|
+
branch: String(record.branch ?? ""),
|
|
1299
|
+
worktree: String(record.workspace ?? record.worktree ?? ""),
|
|
1300
|
+
repos: changed,
|
|
1301
|
+
...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {}
|
|
1302
|
+
};
|
|
1303
|
+
if (state === "integrated") {
|
|
1304
|
+
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
landed.sort((a, b) => a.order - b.order);
|
|
1308
|
+
const entry = ({ taskId, order, title }) => ({ taskId, order, title });
|
|
1309
|
+
return {
|
|
1310
|
+
handoff: {
|
|
1311
|
+
repos: repos.map((repo) => ({ ...repo, landed: landed.filter((t) => t.changed.includes(repo.path)).map(entry) })),
|
|
1312
|
+
landed: landed.map(entry)
|
|
1313
|
+
},
|
|
1314
|
+
tasks
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
function handoffBranch(handoff) {
|
|
1318
|
+
return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
|
|
1319
|
+
}
|
|
1320
|
+
function handoffBase(handoff, length) {
|
|
1321
|
+
const group = isRepoGroup(handoff);
|
|
1322
|
+
return handoff.repos.map((r) => `${group ? `${r.path} ` : ""}${r.baseRef.slice(0, length)}`).join(", ");
|
|
1323
|
+
}
|
|
1324
|
+
function isRepoGroup(handoff) {
|
|
1325
|
+
return handoff.repos.some((r) => r.path !== ".");
|
|
1326
|
+
}
|
|
1327
|
+
function reposWithWork(handoff) {
|
|
1328
|
+
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
1329
|
+
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
1330
|
+
}
|
|
1331
|
+
function repoResultLines(handoff) {
|
|
1332
|
+
return handoff.repos.map(({ path, landed }) => {
|
|
1333
|
+
const n = landed.length;
|
|
1334
|
+
return `${path}: ${n === 0 ? "nothing to merge" : `${n} task${n === 1 ? "" : "s"} landed`}`;
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
function taskRepoNames(isolation) {
|
|
1338
|
+
return (isolation?.repos ?? []).filter((r) => r !== ".");
|
|
1339
|
+
}
|
|
1340
|
+
function mergeOutcome(result, branch, group) {
|
|
1341
|
+
const ok = result.outcome === "merged";
|
|
1342
|
+
if (!group && result.outcome === "conflict") {
|
|
1343
|
+
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.` };
|
|
1344
|
+
}
|
|
1345
|
+
const { message } = (0, import_core7.describeMergeResult)(result, branch, group);
|
|
1346
|
+
return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1179
1349
|
// src/utils/output.ts
|
|
1180
1350
|
function iconFor(status) {
|
|
1181
1351
|
switch (status) {
|
|
@@ -1193,7 +1363,98 @@ function iconFor(status) {
|
|
|
1193
1363
|
}
|
|
1194
1364
|
|
|
1195
1365
|
// src/commands/run.ts
|
|
1196
|
-
var
|
|
1366
|
+
var import_core8 = require("@ordewell/core");
|
|
1367
|
+
|
|
1368
|
+
// src/catalog.ts
|
|
1369
|
+
function normalizeCatalog(result) {
|
|
1370
|
+
const runnersByModel = /* @__PURE__ */ new Map();
|
|
1371
|
+
for (const [runner, entries] of Object.entries(result.modelsByRunner ?? {})) {
|
|
1372
|
+
for (const entry of entries ?? []) {
|
|
1373
|
+
const id = String(entry.modelId ?? entry.id);
|
|
1374
|
+
runnersByModel.set(id, [.../* @__PURE__ */ new Set([...runnersByModel.get(id) ?? [], runner])]);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const models = (result.models ?? []).map((m) => {
|
|
1378
|
+
const id = String(m.modelId ?? m.id);
|
|
1379
|
+
return {
|
|
1380
|
+
id,
|
|
1381
|
+
label: String(m.modelLabel ?? m.label ?? id),
|
|
1382
|
+
provider: String(m.runnerProviderLabel ?? m.runnerProvider ?? m.provider ?? ""),
|
|
1383
|
+
pricing: m.pricing ? `$${m.pricing}/MTok` : void 0,
|
|
1384
|
+
variants: Array.isArray(m.variants) ? m.variants.map((v) => ({ id: String(v.id), label: String(v.label ?? v.id) })) : [],
|
|
1385
|
+
runners: runnersByModel.get(id) ?? []
|
|
1386
|
+
};
|
|
1387
|
+
});
|
|
1388
|
+
const orchestratorModels = (result.orchestratorModels ?? []).map((m) => ({
|
|
1389
|
+
id: String(m.id),
|
|
1390
|
+
label: String(m.label ?? m.id),
|
|
1391
|
+
provider: String(m.provider ?? ""),
|
|
1392
|
+
pricing: m.pricing ? `$${m.pricing}/MTok` : void 0
|
|
1393
|
+
}));
|
|
1394
|
+
return {
|
|
1395
|
+
models,
|
|
1396
|
+
orchestratorModels,
|
|
1397
|
+
providers: result.providers ?? [],
|
|
1398
|
+
providerErrors: result.providerErrors ?? {},
|
|
1399
|
+
modesByRunner: Object.fromEntries(
|
|
1400
|
+
Object.entries(result.modesByRunner ?? {}).map(([runner, modes]) => [
|
|
1401
|
+
runner,
|
|
1402
|
+
(modes ?? []).map((m) => ({
|
|
1403
|
+
id: String(m.id),
|
|
1404
|
+
label: String(m.label ?? m.id),
|
|
1405
|
+
description: m.description ? String(m.description) : void 0,
|
|
1406
|
+
autonomous: m.autonomous ? true : void 0
|
|
1407
|
+
}))
|
|
1408
|
+
])
|
|
1409
|
+
)
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/commands/shared.ts
|
|
1414
|
+
async function connect(subArgs, injected) {
|
|
1415
|
+
return injected ?? new ApiClient(await ensureDaemon(resolvePort(subArgs)));
|
|
1416
|
+
}
|
|
1417
|
+
async function fetchCatalog(api) {
|
|
1418
|
+
return normalizeCatalog(await api.getModels());
|
|
1419
|
+
}
|
|
1420
|
+
function fail(...lines) {
|
|
1421
|
+
for (const line of lines) console.error(line);
|
|
1422
|
+
process.exit(1);
|
|
1423
|
+
}
|
|
1424
|
+
function writeEnv(env) {
|
|
1425
|
+
const envFile = findEnvFile();
|
|
1426
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1427
|
+
writeEnvVar(envFile, key, value);
|
|
1428
|
+
process.env[key] = value;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function persistEnv(api, env) {
|
|
1432
|
+
const settings = await api.updateSettings({ env });
|
|
1433
|
+
writeEnv(env);
|
|
1434
|
+
return settings;
|
|
1435
|
+
}
|
|
1436
|
+
function toTaskView(task) {
|
|
1437
|
+
return {
|
|
1438
|
+
id: task.id,
|
|
1439
|
+
order: task.order,
|
|
1440
|
+
title: task.title,
|
|
1441
|
+
description: task.description,
|
|
1442
|
+
prompt: task.prompt ?? void 0,
|
|
1443
|
+
type: task.type === "user" ? "user" : "ai",
|
|
1444
|
+
status: task.status ?? "pending",
|
|
1445
|
+
dependencies: task.dependencies ?? [],
|
|
1446
|
+
assignedRunner: task.assignedRunner || void 0,
|
|
1447
|
+
taskMode: task.taskMode || void 0,
|
|
1448
|
+
assignedModel: task.assignedModel ?? void 0,
|
|
1449
|
+
subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function taskViews(plan) {
|
|
1453
|
+
const tasks = allTasksOf(plan);
|
|
1454
|
+
return [...tasks].sort((a, b) => a.order - b.order).map(toTaskView);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/commands/run.ts
|
|
1197
1458
|
function resolveSessionId(subArgs) {
|
|
1198
1459
|
const explicit = flag(subArgs, "--session-id");
|
|
1199
1460
|
if (explicit) return explicit;
|
|
@@ -1206,7 +1467,9 @@ function resolveSessionId(subArgs) {
|
|
|
1206
1467
|
console.error(`Using last session: ${last.sessionId} ("${last.goal}")`);
|
|
1207
1468
|
return last.sessionId;
|
|
1208
1469
|
}
|
|
1209
|
-
async function followExecution(api, sessionId) {
|
|
1470
|
+
async function followExecution(api, sessionId, start, onBlocked) {
|
|
1471
|
+
let blocked = null;
|
|
1472
|
+
let dirtyRepos = [];
|
|
1210
1473
|
const taskStates = /* @__PURE__ */ new Map();
|
|
1211
1474
|
let lastPrinted = "";
|
|
1212
1475
|
function printStatus(tasks) {
|
|
@@ -1230,55 +1493,87 @@ async function followExecution(api, sessionId) {
|
|
|
1230
1493
|
lastPrinted = output;
|
|
1231
1494
|
}
|
|
1232
1495
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1496
|
+
let settleReady = () => {
|
|
1497
|
+
};
|
|
1498
|
+
const streamReady = new Promise((resolve3, reject) => {
|
|
1499
|
+
settleReady = (error) => error ? reject(error) : resolve3();
|
|
1500
|
+
});
|
|
1501
|
+
const stream = api.streamExecution(sessionId, (event) => {
|
|
1502
|
+
if (event.type === "task_started") {
|
|
1503
|
+
process.stderr.write(`[${event.order}/${event.title}] Started: ${event.runner} / ${event.modelId}
|
|
1237
1504
|
`);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1505
|
+
}
|
|
1506
|
+
if (event.type === "checkpoint") {
|
|
1507
|
+
process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core8.truncateCheckpointSummary)(event.summary)}
|
|
1241
1508
|
`);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1509
|
+
}
|
|
1510
|
+
if (event.type === "status_update" && event.tasks) {
|
|
1511
|
+
printStatus(event.tasks);
|
|
1512
|
+
}
|
|
1513
|
+
if (event.type === "review_needed") {
|
|
1514
|
+
console.log("\nPlan needs your sign-off \u2014 run `ordewell approve` to continue.");
|
|
1515
|
+
}
|
|
1516
|
+
if (event.type === "execution_complete") {
|
|
1517
|
+
const s = event;
|
|
1518
|
+
const summary = s.summary || { total: 0 };
|
|
1519
|
+
console.log(
|
|
1520
|
+
`
|
|
1254
1521
|
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
|
-
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
if (event.type === "execution_stopped") {
|
|
1525
|
+
console.log("\nExecution stopped.");
|
|
1526
|
+
}
|
|
1527
|
+
if (event.type === "notice") console.error(`\xB7 ${event.message}`);
|
|
1528
|
+
if (event.type === "isolation_blocked") {
|
|
1529
|
+
blocked = event.message;
|
|
1530
|
+
dirtyRepos = event.repos ?? [];
|
|
1531
|
+
}
|
|
1532
|
+
if (event.type === "isolation_handoff") {
|
|
1533
|
+
const n = event.landed.length;
|
|
1534
|
+
const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
|
|
1535
|
+
console.log(`
|
|
1536
|
+
Run finished on ${handoffBranch(event)}${where} \u2014 ${n} task${n === 1 ? "" : "s"} landed.`);
|
|
1537
|
+
if (isRepoGroup(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
|
|
1538
|
+
console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
|
|
1539
|
+
}
|
|
1540
|
+
}, settleReady);
|
|
1541
|
+
const streamFailed = (err) => {
|
|
1262
1542
|
console.error(`Execution stream error: ${err.message}`);
|
|
1263
1543
|
process.exit(1);
|
|
1544
|
+
};
|
|
1545
|
+
void stream.catch(settleReady);
|
|
1546
|
+
await streamReady.catch(streamFailed);
|
|
1547
|
+
await start();
|
|
1548
|
+
await stream.catch(streamFailed);
|
|
1549
|
+
if (blocked === null) return;
|
|
1550
|
+
if (!onBlocked) {
|
|
1551
|
+
await api.stopExecution(sessionId);
|
|
1552
|
+
console.error(blocked);
|
|
1553
|
+
const where = dirtyRepos.length > 0 ? ` in ${dirtyRepos.join(", ")}` : "";
|
|
1554
|
+
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.`);
|
|
1555
|
+
process.exit(1);
|
|
1264
1556
|
}
|
|
1557
|
+
await followExecution(api, sessionId, () => onBlocked === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
|
|
1265
1558
|
}
|
|
1266
|
-
async function handleRun(subArgs) {
|
|
1559
|
+
async function handleRun(subArgs, injectedApi) {
|
|
1267
1560
|
const sessionId = resolveSessionId(subArgs);
|
|
1268
|
-
const api =
|
|
1561
|
+
const api = await connect(subArgs, injectedApi);
|
|
1269
1562
|
console.error(`Executing plan...`);
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1563
|
+
const onBlocked = hasFlag(subArgs, "--stash") ? "stash" : hasFlag(subArgs, "--without-isolation") ? "shared" : void 0;
|
|
1564
|
+
await followExecution(api, sessionId, async () => {
|
|
1565
|
+
try {
|
|
1566
|
+
await api.executePlan(sessionId);
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
console.error(`Failed to start execution: ${err.message}`);
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
}
|
|
1571
|
+
}, onBlocked);
|
|
1277
1572
|
}
|
|
1278
1573
|
|
|
1279
1574
|
// src/commands/status.ts
|
|
1280
1575
|
var import_fs4 = require("fs");
|
|
1281
|
-
var
|
|
1576
|
+
var import_core9 = require("@ordewell/core");
|
|
1282
1577
|
async function handleStatus(subArgs, injectedApi) {
|
|
1283
1578
|
const sessionId = flag(subArgs, "--session-id");
|
|
1284
1579
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
@@ -1349,12 +1644,12 @@ function printOneSession(sessionId, meta, plan) {
|
|
|
1349
1644
|
const icon = iconFor(t.status);
|
|
1350
1645
|
const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
|
|
1351
1646
|
const shortId = (t.id ?? "").slice(-8);
|
|
1352
|
-
console.log(` ${(0,
|
|
1647
|
+
console.log(` ${(0, import_core9.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
|
|
1353
1648
|
for (const sub of t.subtasks || []) {
|
|
1354
1649
|
const subIcon = iconFor(sub.status);
|
|
1355
1650
|
const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
|
|
1356
1651
|
const subShortId = (sub.id ?? "").slice(-8);
|
|
1357
|
-
console.log(` ${(0,
|
|
1652
|
+
console.log(` ${(0, import_core9.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
|
|
1358
1653
|
}
|
|
1359
1654
|
}
|
|
1360
1655
|
}
|
|
@@ -1406,123 +1701,32 @@ async function handleWeb(subArgs) {
|
|
|
1406
1701
|
}
|
|
1407
1702
|
|
|
1408
1703
|
// src/commands/models.ts
|
|
1409
|
-
var
|
|
1704
|
+
var import_core11 = require("@ordewell/core");
|
|
1410
1705
|
|
|
1411
1706
|
// 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
|
-
);
|
|
1707
|
+
var import_core10 = require("@ordewell/core");
|
|
1708
|
+
var USAGE = "Usage: ordewell model [show | set <model-id>]";
|
|
1709
|
+
async function handleModel(subArgs, injectedApi) {
|
|
1710
|
+
const [action, modelId] = positionals(subArgs);
|
|
1711
|
+
if (action && action !== "show" && action !== "set") {
|
|
1712
|
+
fail(`Unknown model action: "${action}"`, USAGE);
|
|
1713
|
+
}
|
|
1714
|
+
const api = await connect(subArgs, injectedApi);
|
|
1715
|
+
if (action !== "set") {
|
|
1716
|
+
await showModels(api);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (!modelId) fail(USAGE);
|
|
1720
|
+
const settings = await api.getSettings();
|
|
1721
|
+
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1722
|
+
const runner = (0, import_core10.runnerForProvider)(provider);
|
|
1723
|
+
if (runner) {
|
|
1724
|
+
const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
|
|
1725
|
+
if (known.length > 0 && !known.some((m) => m.id === modelId)) {
|
|
1726
|
+
fail(
|
|
1727
|
+
`${modelId} was not discovered for ${runner}.`,
|
|
1728
|
+
`Run \`ordewell model\` to see what ${runner} offers.`
|
|
1729
|
+
);
|
|
1526
1730
|
}
|
|
1527
1731
|
}
|
|
1528
1732
|
await api.updateSettings({ orchestratorModel: modelId });
|
|
@@ -1534,7 +1738,7 @@ async function showModels(api) {
|
|
|
1534
1738
|
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
1535
1739
|
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
1536
1740
|
const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
1537
|
-
const runner = (0,
|
|
1741
|
+
const runner = (0, import_core10.runnerForProvider)(provider);
|
|
1538
1742
|
const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
|
|
1539
1743
|
console.log(`
|
|
1540
1744
|
Planner model: ${current || "(backend default)"}`);
|
|
@@ -1568,8 +1772,8 @@ async function handleModels(subArgs) {
|
|
|
1568
1772
|
const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
|
|
1569
1773
|
return handleModel(["set", subArgs[setIdx + 1], ...rest]);
|
|
1570
1774
|
}
|
|
1571
|
-
const config = new
|
|
1572
|
-
const providers = (0,
|
|
1775
|
+
const config = new import_core11.EnvConfig();
|
|
1776
|
+
const providers = (0, import_core11.configuredProviders)(config);
|
|
1573
1777
|
if (providers.length === 0) {
|
|
1574
1778
|
console.error("No providers configured. Set an API key for a provider, e.g.:");
|
|
1575
1779
|
console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
|
|
@@ -1578,19 +1782,19 @@ async function handleModels(subArgs) {
|
|
|
1578
1782
|
console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
|
|
1579
1783
|
process.exit(1);
|
|
1580
1784
|
}
|
|
1581
|
-
const { apiKeys, baseUrls } = (0,
|
|
1785
|
+
const { apiKeys, baseUrls } = (0, import_core11.collectProviderCredentials)(config);
|
|
1582
1786
|
let models;
|
|
1583
1787
|
let errors;
|
|
1584
1788
|
try {
|
|
1585
|
-
const result = await (0,
|
|
1586
|
-
models = (0,
|
|
1789
|
+
const result = await (0, import_core11.fetchAllProviderModels)({ apiKeys, baseUrls });
|
|
1790
|
+
models = (0, import_core11.toOrchestratorOptions)(result.models, import_core11.ORCHESTRATOR_SHORTCUTS);
|
|
1587
1791
|
errors = result.errors;
|
|
1588
1792
|
} catch (err) {
|
|
1589
1793
|
console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
|
|
1590
1794
|
process.exit(1);
|
|
1591
1795
|
return;
|
|
1592
1796
|
}
|
|
1593
|
-
const providerLabel = (p) => (0,
|
|
1797
|
+
const providerLabel = (p) => (0, import_core11.getProviderMeta)(p)?.label ?? p;
|
|
1594
1798
|
console.log(
|
|
1595
1799
|
`
|
|
1596
1800
|
Available models (${models.length}) across ${providers.length} configured provider(s):`
|
|
@@ -1707,9 +1911,9 @@ function handleSetup(subArgs) {
|
|
|
1707
1911
|
}
|
|
1708
1912
|
|
|
1709
1913
|
// src/commands/plugins.ts
|
|
1710
|
-
var
|
|
1914
|
+
var import_core12 = require("@ordewell/core");
|
|
1711
1915
|
function handlePlugins(subArgs) {
|
|
1712
|
-
const registry = new
|
|
1916
|
+
const registry = new import_core12.RunnerRegistry();
|
|
1713
1917
|
registry.loadUserPlugins();
|
|
1714
1918
|
const sub = subArgs[0];
|
|
1715
1919
|
if (sub === "list" || !sub) {
|
|
@@ -1745,7 +1949,7 @@ ${plugins.length} plugin(s):
|
|
|
1745
1949
|
process.exit(1);
|
|
1746
1950
|
}
|
|
1747
1951
|
try {
|
|
1748
|
-
const resolved = (0,
|
|
1952
|
+
const resolved = (0, import_core12.classifyPluginSource)(source);
|
|
1749
1953
|
const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
|
|
1750
1954
|
console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
|
|
1751
1955
|
} catch (err) {
|
|
@@ -2132,15 +2336,477 @@ async function handleSessions(subArgs, injectedApi) {
|
|
|
2132
2336
|
console.error(`Failed to delete session: ${err.message}`);
|
|
2133
2337
|
process.exit(1);
|
|
2134
2338
|
}
|
|
2135
|
-
return;
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
console.error(`Unknown sessions action: "${action}"`);
|
|
2342
|
+
console.error(USAGE3);
|
|
2343
|
+
process.exit(1);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/commands/conversation.ts
|
|
2347
|
+
var REWIND_USAGE = "Usage: ordewell rewind [<n>] [--session-id <id>] [--workspace /path] \u2014 with no <n>, lists the messages you can rewind to.";
|
|
2348
|
+
async function adopted(subArgs, injectedApi) {
|
|
2349
|
+
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
2350
|
+
const sessionId = resolveSessionId(subArgs);
|
|
2351
|
+
const api = await connect(subArgs, injectedApi);
|
|
2352
|
+
let plan;
|
|
2353
|
+
try {
|
|
2354
|
+
({ plan } = await api.adoptSession(sessionId, workspace));
|
|
2355
|
+
} catch (err) {
|
|
2356
|
+
fail(`Failed to load session: ${err.message}`);
|
|
2357
|
+
}
|
|
2358
|
+
return { api, sessionId, workspace, plan };
|
|
2359
|
+
}
|
|
2360
|
+
async function handleFork(subArgs, injectedApi) {
|
|
2361
|
+
const { api, sessionId, workspace } = await adopted(subArgs, injectedApi);
|
|
2362
|
+
let fork;
|
|
2363
|
+
try {
|
|
2364
|
+
fork = await api.forkConversation(sessionId);
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
fail(`Failed to fork: ${err.message}`);
|
|
2367
|
+
}
|
|
2368
|
+
saveLastSession(fork.sessionId, fork.goal, fork.plan.runners ?? [], workspace);
|
|
2369
|
+
console.log(`Forked ${sessionId} into ${fork.sessionId} \u2014 the fork is now the current session.`);
|
|
2370
|
+
console.log(` \`ordewell sessions load ${sessionId}\` to go back to the original.`);
|
|
2371
|
+
}
|
|
2372
|
+
async function handleCompact(subArgs, injectedApi) {
|
|
2373
|
+
const { api, sessionId } = await adopted(subArgs, injectedApi);
|
|
2374
|
+
let result;
|
|
2375
|
+
try {
|
|
2376
|
+
result = await api.compactConversation(sessionId);
|
|
2377
|
+
} catch (err) {
|
|
2378
|
+
fail(`Failed to condense: ${err.message}`);
|
|
2379
|
+
}
|
|
2380
|
+
console.log(`Condensed ${sessionId}. The last two exchanges were kept as they were; the tasks are unchanged.
|
|
2381
|
+
`);
|
|
2382
|
+
console.log(result.summary);
|
|
2383
|
+
}
|
|
2384
|
+
async function handleRewind(subArgs, injectedApi) {
|
|
2385
|
+
const arg = positionals(subArgs)[0];
|
|
2386
|
+
if (arg !== void 0 && !/^\d+$/.test(arg)) fail(REWIND_USAGE);
|
|
2387
|
+
const { api, sessionId } = await adopted(subArgs, injectedApi);
|
|
2388
|
+
if (arg === void 0) {
|
|
2389
|
+
let targets;
|
|
2390
|
+
try {
|
|
2391
|
+
targets = await api.rewindTargets(sessionId);
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
fail(`Failed to list messages: ${err.message}`);
|
|
2394
|
+
}
|
|
2395
|
+
if (targets.length === 0) {
|
|
2396
|
+
console.log("Nothing to rewind to yet \u2014 the only message so far is the goal.");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
console.log("Your messages, most recent first:\n");
|
|
2400
|
+
for (const t of [...targets].reverse()) console.log(` ${String(t.index).padStart(4)} ${t.preview}`);
|
|
2401
|
+
console.log("\n `ordewell rewind <n>` discards message <n> and everything after it. The tasks stay as they are.");
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
try {
|
|
2405
|
+
await api.rewindConversation(sessionId, Number(arg));
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
fail(`Failed to rewind: ${err.message}`);
|
|
2408
|
+
}
|
|
2409
|
+
console.log(`Rewound ${sessionId} to just before message ${arg}. The tasks are unchanged; the next message continues from there.`);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// src/commands/handoff.ts
|
|
2413
|
+
var import_readline2 = require("readline");
|
|
2414
|
+
|
|
2415
|
+
// src/tui/ansi.ts
|
|
2416
|
+
var import_string_width = __toESM(require("string-width"));
|
|
2417
|
+
var ESCAPE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\|$)|[P^_][\s\S]*?(?:\x1b\\|$)|\[[0-?]*[ -/]*[@-~]?|[ -/]*[0-~]?)/g;
|
|
2418
|
+
var ESCAPE_SPLIT = new RegExp(`(${ESCAPE.source})`);
|
|
2419
|
+
var CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
|
|
2420
|
+
var CONTROL_IN_ROW = /[\x00-\x1f\x7f-\x9f]/g;
|
|
2421
|
+
var HAS_CONTROL = /[\x00-\x1f\x7f-\x9f]/;
|
|
2422
|
+
var PAINT_ESCAPE = /^\x1b\[[0-9;]*[mGK]$/;
|
|
2423
|
+
function stripAnsi(text) {
|
|
2424
|
+
return text.replace(ESCAPE, "");
|
|
2425
|
+
}
|
|
2426
|
+
function sanitize(text) {
|
|
2427
|
+
return text.replace(ESCAPE, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(CONTROL, "");
|
|
2428
|
+
}
|
|
2429
|
+
function paintOnly(line) {
|
|
2430
|
+
if (!HAS_CONTROL.test(line)) return line;
|
|
2431
|
+
return line.split(ESCAPE_SPLIT).map((piece) => piece.startsWith("\x1B") ? PAINT_ESCAPE.test(piece) ? piece : "" : piece.replace(CONTROL_IN_ROW, "")).join("");
|
|
2432
|
+
}
|
|
2433
|
+
function width(text) {
|
|
2434
|
+
return (0, import_string_width.default)(stripAnsi(text));
|
|
2435
|
+
}
|
|
2436
|
+
function truncate(text, max) {
|
|
2437
|
+
if (max <= 0) return "";
|
|
2438
|
+
if (width(text) <= max) return text;
|
|
2439
|
+
let out = "";
|
|
2440
|
+
let used = 0;
|
|
2441
|
+
for (const char of stripAnsi(text)) {
|
|
2442
|
+
const w = (0, import_string_width.default)(char);
|
|
2443
|
+
if (used + w > max - 1) break;
|
|
2444
|
+
out += char;
|
|
2445
|
+
used += w;
|
|
2446
|
+
}
|
|
2447
|
+
return out + "\u2026";
|
|
2448
|
+
}
|
|
2449
|
+
function pad(text, target) {
|
|
2450
|
+
const current = width(text);
|
|
2451
|
+
if (current > target) return truncate(text, target);
|
|
2452
|
+
return text + " ".repeat(target - current);
|
|
2453
|
+
}
|
|
2454
|
+
function chopWord(piece, max) {
|
|
2455
|
+
const chunks = [];
|
|
2456
|
+
let cur = "";
|
|
2457
|
+
let curWidth = 0;
|
|
2458
|
+
for (const ch of piece) {
|
|
2459
|
+
const w = (0, import_string_width.default)(ch);
|
|
2460
|
+
if (cur && curWidth + w > max) {
|
|
2461
|
+
chunks.push(cur);
|
|
2462
|
+
cur = ch;
|
|
2463
|
+
curWidth = w;
|
|
2464
|
+
} else {
|
|
2465
|
+
cur += ch;
|
|
2466
|
+
curWidth += w;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
chunks.push(cur);
|
|
2470
|
+
return chunks;
|
|
2471
|
+
}
|
|
2472
|
+
var wrapMemo = /* @__PURE__ */ new Map();
|
|
2473
|
+
var WRAP_MEMO_MAX = 64;
|
|
2474
|
+
function wrapLines(text, max) {
|
|
2475
|
+
const key = `${max}\0${text}`;
|
|
2476
|
+
const cached = wrapMemo.get(key);
|
|
2477
|
+
if (cached) {
|
|
2478
|
+
wrapMemo.delete(key);
|
|
2479
|
+
wrapMemo.set(key, cached);
|
|
2480
|
+
return cached;
|
|
2481
|
+
}
|
|
2482
|
+
const result = computeWrapLines(text, max);
|
|
2483
|
+
if (wrapMemo.size >= WRAP_MEMO_MAX) {
|
|
2484
|
+
const oldest = wrapMemo.keys().next().value;
|
|
2485
|
+
if (oldest !== void 0) wrapMemo.delete(oldest);
|
|
2486
|
+
}
|
|
2487
|
+
wrapMemo.set(key, result);
|
|
2488
|
+
return result;
|
|
2489
|
+
}
|
|
2490
|
+
function computeWrapLines(text, max) {
|
|
2491
|
+
if (max <= 0) return [{ line: "", start: 0 }];
|
|
2492
|
+
const out = [];
|
|
2493
|
+
let pos = 0;
|
|
2494
|
+
for (const paragraph of text.split("\n")) {
|
|
2495
|
+
let current = "";
|
|
2496
|
+
let currentStart = pos;
|
|
2497
|
+
let first = true;
|
|
2498
|
+
let wordPos = pos;
|
|
2499
|
+
for (const word of paragraph.split(" ")) {
|
|
2500
|
+
let piece = word;
|
|
2501
|
+
let pieceStart = wordPos;
|
|
2502
|
+
if (width(piece) > max) {
|
|
2503
|
+
const chunks = chopWord(piece, max);
|
|
2504
|
+
for (let ci = 0; ci < chunks.length - 1; ci++) {
|
|
2505
|
+
const head = chunks[ci];
|
|
2506
|
+
if (!first) {
|
|
2507
|
+
out.push({ line: current, start: currentStart });
|
|
2508
|
+
current = "";
|
|
2509
|
+
}
|
|
2510
|
+
out.push({ line: head, start: pieceStart });
|
|
2511
|
+
pieceStart += head.length;
|
|
2512
|
+
first = true;
|
|
2513
|
+
}
|
|
2514
|
+
piece = chunks[chunks.length - 1];
|
|
2515
|
+
}
|
|
2516
|
+
const candidate = first ? piece : `${current} ${piece}`;
|
|
2517
|
+
const candidateStart = first ? pieceStart : currentStart;
|
|
2518
|
+
first = false;
|
|
2519
|
+
if (width(candidate) > max) {
|
|
2520
|
+
out.push({ line: current, start: currentStart });
|
|
2521
|
+
current = piece;
|
|
2522
|
+
currentStart = pieceStart;
|
|
2523
|
+
} else {
|
|
2524
|
+
current = candidate;
|
|
2525
|
+
currentStart = candidateStart;
|
|
2526
|
+
}
|
|
2527
|
+
wordPos += word.length + 1;
|
|
2528
|
+
}
|
|
2529
|
+
out.push({ line: current, start: currentStart });
|
|
2530
|
+
pos += paragraph.length + 1;
|
|
2531
|
+
}
|
|
2532
|
+
return out.length > 0 ? out : [{ line: "", start: 0 }];
|
|
2533
|
+
}
|
|
2534
|
+
function wrap(text, max) {
|
|
2535
|
+
return wrapLines(text, max).map((l) => l.line);
|
|
2536
|
+
}
|
|
2537
|
+
function colour(open) {
|
|
2538
|
+
return (text) => style.enabled ? `\x1B[${open}m${text}\x1B[0m` : text;
|
|
2539
|
+
}
|
|
2540
|
+
var style = {
|
|
2541
|
+
/** Set false when the output is piped or the terminal has no colour. */
|
|
2542
|
+
enabled: true,
|
|
2543
|
+
bold: colour("1"),
|
|
2544
|
+
dim: colour("2"),
|
|
2545
|
+
italic: colour("3"),
|
|
2546
|
+
underline: colour("4"),
|
|
2547
|
+
inverse: colour("7"),
|
|
2548
|
+
red: colour("31"),
|
|
2549
|
+
green: colour("32"),
|
|
2550
|
+
yellow: colour("33"),
|
|
2551
|
+
blue: colour("34"),
|
|
2552
|
+
magenta: colour("35"),
|
|
2553
|
+
cyan: colour("36"),
|
|
2554
|
+
grey: colour("90")
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
// src/tui/transcript.ts
|
|
2558
|
+
function say(state, role, content, research, extra) {
|
|
2559
|
+
const message = {
|
|
2560
|
+
role,
|
|
2561
|
+
content: sanitize(content),
|
|
2562
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2563
|
+
...research ? { research } : {},
|
|
2564
|
+
...extra
|
|
2565
|
+
};
|
|
2566
|
+
return { ...state, messages: [...state.messages, message], scroll: 0 };
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// src/tui/handoff.ts
|
|
2570
|
+
var HANDOFF_ACTIONS = [
|
|
2571
|
+
{ id: "review", label: "Review diff", hint: "the branch against the commit it forked from" },
|
|
2572
|
+
{ id: "merge", label: "Merge", hint: "into the branch you have checked out \u2014 asks first" },
|
|
2573
|
+
{ id: "discard", label: "Discard", hint: "the run, its worktrees and this branch \u2014 asks first" },
|
|
2574
|
+
{ id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
|
|
2575
|
+
];
|
|
2576
|
+
function handoffActions(handoff) {
|
|
2577
|
+
if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
|
|
2578
|
+
return [
|
|
2579
|
+
{ id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
|
|
2580
|
+
{ id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there \u2014 asks first" },
|
|
2581
|
+
{ id: "discard", label: "Discard", hint: "the run, its worktrees and every repository's branch \u2014 asks first" },
|
|
2582
|
+
{ id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches everywhere, keep the integration branches" }
|
|
2583
|
+
];
|
|
2584
|
+
}
|
|
2585
|
+
var reposOf2 = (handoff) => handoff.repos.map((r) => r.path).join(", ");
|
|
2586
|
+
var HANDOFF_USAGE = `/handoff [${HANDOFF_ACTIONS.map((a) => a.id).join("|")}]`;
|
|
2587
|
+
var diffRoom = (rows) => Math.max(1, rows - 3);
|
|
2588
|
+
var fail2 = (state, content) => ({ state: say(state, "error", content), effects: [] });
|
|
2589
|
+
function handoffCommand(state, arg) {
|
|
2590
|
+
if (!state.sessionId || !state.handoff) return fail2(state, "No isolated run to hand off \u2014 a plan that ran in worktrees leaves one here.");
|
|
2591
|
+
if (arg === void 0) return { state: { ...state, overlay: { kind: "handoff", index: 0, diff: null } }, effects: [] };
|
|
2592
|
+
const action = HANDOFF_ACTIONS.find((a) => a.id === arg.toLowerCase());
|
|
2593
|
+
if (!action) return fail2(state, `Usage: ${HANDOFF_USAGE}`);
|
|
2594
|
+
return runHandoffAction(state, action.id);
|
|
2595
|
+
}
|
|
2596
|
+
function handoffArrived(state, handoff) {
|
|
2597
|
+
const landed = handoff.landed.length;
|
|
2598
|
+
const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
|
|
2599
|
+
const told = say(
|
|
2600
|
+
{ ...state, handoff },
|
|
2601
|
+
"system",
|
|
2602
|
+
`Run finished on ${handoffBranch(handoff)}${where} \u2014 ${landed} task${landed === 1 ? "" : "s"} landed. /handoff to review and land it.`
|
|
2603
|
+
);
|
|
2604
|
+
return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
|
|
2605
|
+
}
|
|
2606
|
+
function runHandoffAction(state, id) {
|
|
2607
|
+
const { sessionId, handoff } = state;
|
|
2608
|
+
if (!sessionId || !handoff) return fail2(state, "No isolated run to hand off.");
|
|
2609
|
+
const closed = { ...state, overlay: null };
|
|
2610
|
+
switch (id) {
|
|
2611
|
+
case "review":
|
|
2612
|
+
return { state, effects: [{ type: "isolationReviewDiff", sessionId }] };
|
|
2613
|
+
case "cleanup":
|
|
2614
|
+
return { state: closed, effects: [{ type: "isolationCleanup", sessionId, branch: handoffBranch(handoff) }] };
|
|
2615
|
+
case "merge":
|
|
2616
|
+
return {
|
|
2617
|
+
state: {
|
|
2618
|
+
...state,
|
|
2619
|
+
overlay: isRepoGroup(handoff) ? {
|
|
2620
|
+
kind: "confirm",
|
|
2621
|
+
title: "Merge all into your branches?",
|
|
2622
|
+
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.`,
|
|
2623
|
+
action: { kind: "merge-run" }
|
|
2624
|
+
} : {
|
|
2625
|
+
kind: "confirm",
|
|
2626
|
+
title: "Merge into your branch?",
|
|
2627
|
+
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.`,
|
|
2628
|
+
action: { kind: "merge-run" }
|
|
2629
|
+
}
|
|
2630
|
+
},
|
|
2631
|
+
effects: []
|
|
2632
|
+
};
|
|
2633
|
+
case "discard":
|
|
2634
|
+
return {
|
|
2635
|
+
state: {
|
|
2636
|
+
...state,
|
|
2637
|
+
overlay: {
|
|
2638
|
+
kind: "confirm",
|
|
2639
|
+
title: "Discard this run?",
|
|
2640
|
+
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.`,
|
|
2641
|
+
action: { kind: "discard-run" }
|
|
2642
|
+
}
|
|
2643
|
+
},
|
|
2644
|
+
effects: []
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
function confirmedHandoff(state, kind) {
|
|
2649
|
+
const closed = { ...state, overlay: null };
|
|
2650
|
+
if (!state.sessionId || !state.handoff) return { state: closed, effects: [] };
|
|
2651
|
+
const { sessionId, handoff } = state;
|
|
2652
|
+
const effect = kind === "merge-run" ? { type: "isolationMerge", sessionId, branch: handoffBranch(handoff), ...isRepoGroup(handoff) ? { group: true } : {} } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
|
|
2653
|
+
return { state: closed, effects: [effect] };
|
|
2654
|
+
}
|
|
2655
|
+
function showDiff(state, diff) {
|
|
2656
|
+
if (diff.trim() === "") return say(state, "system", `Nothing differs from ${state.handoff ? handoffBase(state.handoff, 8) : "the base commit"}.`);
|
|
2657
|
+
const index = state.overlay?.kind === "handoff" ? state.overlay.index : 0;
|
|
2658
|
+
const lines = sanitize(diff.replace(/\t/g, " ")).split("\n");
|
|
2659
|
+
return { ...state, overlay: { kind: "handoff", index, diff: { lines, scroll: 0 } } };
|
|
2660
|
+
}
|
|
2661
|
+
function handleHandoffKey(state, overlay, key, rows) {
|
|
2662
|
+
const hold = { state, effects: [] };
|
|
2663
|
+
const up = key.name === "up" || key.name === "scrollup";
|
|
2664
|
+
const down = key.name === "down" || key.name === "scrolldown";
|
|
2665
|
+
if (overlay.diff) {
|
|
2666
|
+
const { lines, scroll } = overlay.diff;
|
|
2667
|
+
const room = diffRoom(rows);
|
|
2668
|
+
const max = Math.max(0, lines.length - room);
|
|
2669
|
+
const delta = up ? -1 : down ? 1 : key.name === "pageup" ? -room : key.name === "pagedown" ? room : null;
|
|
2670
|
+
if (delta !== null) {
|
|
2671
|
+
const next = Math.min(max, Math.max(0, scroll + delta));
|
|
2672
|
+
return { state: { ...state, overlay: { ...overlay, diff: { lines, scroll: next } } }, effects: [] };
|
|
2673
|
+
}
|
|
2674
|
+
if (key.name === "escape" || key.name === "enter") return { state: { ...state, overlay: { ...overlay, diff: null } }, effects: [] };
|
|
2675
|
+
return hold;
|
|
2676
|
+
}
|
|
2677
|
+
if (key.name === "escape") return { state: { ...state, overlay: null }, effects: [] };
|
|
2678
|
+
if (up || down) {
|
|
2679
|
+
const index = Math.min(HANDOFF_ACTIONS.length - 1, Math.max(0, overlay.index + (up ? -1 : 1)));
|
|
2680
|
+
return { state: { ...state, overlay: { ...overlay, index } }, effects: [] };
|
|
2681
|
+
}
|
|
2682
|
+
if (key.name === "enter") return runHandoffAction(state, HANDOFF_ACTIONS[overlay.index].id);
|
|
2683
|
+
return hold;
|
|
2684
|
+
}
|
|
2685
|
+
var BLOCKED_ITEMS = [
|
|
2686
|
+
{ id: "stash", label: "Stash and continue", detail: "git stash your tracked changes, then run in worktrees" },
|
|
2687
|
+
{ id: "shared", label: "Run without isolation", detail: "this run only, in your working tree" },
|
|
2688
|
+
{ id: "cancel", label: "Cancel", detail: "start nothing" }
|
|
2689
|
+
];
|
|
2690
|
+
function blockedPicker(state, message, repos = []) {
|
|
2691
|
+
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);
|
|
2692
|
+
return {
|
|
2693
|
+
...state,
|
|
2694
|
+
overlay: {
|
|
2695
|
+
kind: "picker",
|
|
2696
|
+
picker: {
|
|
2697
|
+
title: "Run blocked by uncommitted changes",
|
|
2698
|
+
hint: message,
|
|
2699
|
+
items,
|
|
2700
|
+
filter: "",
|
|
2701
|
+
index: 0,
|
|
2702
|
+
multi: false,
|
|
2703
|
+
chosen: [],
|
|
2704
|
+
action: { kind: "isolation-blocked" }
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
function chooseBlocked(state, choice) {
|
|
2710
|
+
const closed = { ...state, overlay: null };
|
|
2711
|
+
if (!state.sessionId) return { state: closed, effects: [] };
|
|
2712
|
+
const { sessionId } = state;
|
|
2713
|
+
if (choice === "cancel") return { state: closed, effects: [{ type: "stopExecution", sessionId }] };
|
|
2714
|
+
return { state: closed, effects: [{ type: "isolationContinue", sessionId, mode: choice }] };
|
|
2715
|
+
}
|
|
2716
|
+
function sameIsolation(a, b) {
|
|
2717
|
+
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");
|
|
2718
|
+
}
|
|
2719
|
+
function mapTasks(tasks, map) {
|
|
2720
|
+
return tasks.map((task) => {
|
|
2721
|
+
const mapped = map(task);
|
|
2722
|
+
return task.subtasks ? { ...mapped, subtasks: mapTasks(task.subtasks, map) } : mapped;
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
function clearIsolation(state) {
|
|
2726
|
+
return {
|
|
2727
|
+
...state,
|
|
2728
|
+
handoff: null,
|
|
2729
|
+
tasks: mapTasks(state.tasks, ({ isolation: _dropped, ...task }) => task)
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
function isolationForPlan(tasks, record, previous) {
|
|
2733
|
+
const known = /* @__PURE__ */ new Map();
|
|
2734
|
+
mapTasks(previous, (task) => {
|
|
2735
|
+
if (task.isolation) known.set(task.id, task.isolation);
|
|
2736
|
+
return task;
|
|
2737
|
+
});
|
|
2738
|
+
return mapTasks(tasks, (task) => {
|
|
2739
|
+
const isolation = record ? record.tasks[task.id] ?? { state: "none" } : known.get(task.id);
|
|
2740
|
+
return isolation ? { ...task, isolation } : task;
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// src/commands/handoff.ts
|
|
2745
|
+
var USAGE4 = "Usage: ordewell handoff [review|merge|discard|cleanup] [--session-id <id>] [--workspace /path] [--yes]";
|
|
2746
|
+
async function askYesNo(question) {
|
|
2747
|
+
if (!process.stdin.isTTY) return false;
|
|
2748
|
+
const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
|
|
2749
|
+
try {
|
|
2750
|
+
const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
|
|
2751
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
2752
|
+
} finally {
|
|
2753
|
+
rl.close();
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
2757
|
+
const action = positionals(subArgs)[0]?.toLowerCase();
|
|
2758
|
+
if (action === void 0) {
|
|
2759
|
+
console.log(`${USAGE4}
|
|
2760
|
+
`);
|
|
2761
|
+
for (const a of HANDOFF_ACTIONS) console.log(` ${a.id.padEnd(8)} ${a.label} \u2014 ${a.hint}`);
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
if (!HANDOFF_ACTIONS.some((a) => a.id === action)) fail(`Unknown handoff action "${action}".`, USAGE4);
|
|
2765
|
+
const { api, sessionId, plan } = await adopted(subArgs, injectedApi);
|
|
2766
|
+
const handoff = isolationOfPlan(plan)?.handoff;
|
|
2767
|
+
if (!handoff) fail("This session has no isolated run to hand off.");
|
|
2768
|
+
const branch = handoffBranch(handoff);
|
|
2769
|
+
const group = isRepoGroup(handoff);
|
|
2770
|
+
if (group) for (const line of repoResultLines(handoff)) console.error(line);
|
|
2771
|
+
const asked = hasFlag(subArgs, "--yes");
|
|
2772
|
+
const confirmed = async (question) => {
|
|
2773
|
+
if (asked || await confirm(question)) return;
|
|
2774
|
+
fail("Not confirmed \u2014 nothing was changed. Pass --yes to confirm without a prompt.");
|
|
2775
|
+
};
|
|
2776
|
+
const attempt = async (work) => {
|
|
2777
|
+
try {
|
|
2778
|
+
return await work();
|
|
2779
|
+
} catch (err) {
|
|
2780
|
+
return fail(`Handoff ${action} failed: ${err.message}`);
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
switch (action) {
|
|
2784
|
+
case "review": {
|
|
2785
|
+
const diff = await attempt(() => api.reviewRunDiff(sessionId));
|
|
2786
|
+
console.log(diff.trim() === "" ? `Nothing differs from ${handoffBase(handoff, 8)}.` : diff);
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
case "merge": {
|
|
2790
|
+
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.` : `Merge ${branch} into the branch you have checked out?`);
|
|
2791
|
+
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group);
|
|
2792
|
+
if (ok) console.log(message);
|
|
2793
|
+
else fail(message);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
case "discard":
|
|
2797
|
+
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}?`);
|
|
2798
|
+
await attempt(() => api.discardRun(sessionId));
|
|
2799
|
+
console.log(`Discarded the run and ${branch}${group ? " in every repository" : ""}.`);
|
|
2800
|
+
return;
|
|
2801
|
+
case "cleanup":
|
|
2802
|
+
await attempt(() => api.cleanupRun(sessionId));
|
|
2803
|
+
console.log(`Removed the run's worktrees and task branches; ${branch} is kept${group ? " in every repository" : ""}.`);
|
|
2804
|
+
return;
|
|
2136
2805
|
}
|
|
2137
|
-
console.error(`Unknown sessions action: "${action}"`);
|
|
2138
|
-
console.error(USAGE3);
|
|
2139
|
-
process.exit(1);
|
|
2140
2806
|
}
|
|
2141
2807
|
|
|
2142
2808
|
// src/commands/planner.ts
|
|
2143
|
-
var
|
|
2809
|
+
var import_core13 = require("@ordewell/core");
|
|
2144
2810
|
|
|
2145
2811
|
// src/plannerModelSwitch.ts
|
|
2146
2812
|
function describePlannerSwitch(settings, provider) {
|
|
@@ -2159,7 +2825,7 @@ function rememberedModelFor(settings, provider) {
|
|
|
2159
2825
|
}
|
|
2160
2826
|
|
|
2161
2827
|
// src/commands/planner.ts
|
|
2162
|
-
var KNOWN_PROVIDERS = Object.keys(
|
|
2828
|
+
var KNOWN_PROVIDERS = Object.keys(import_core13.ALL_PROVIDERS);
|
|
2163
2829
|
async function handlePlanner(subArgs, injectedApi) {
|
|
2164
2830
|
const api = await connect(subArgs, injectedApi);
|
|
2165
2831
|
const [wanted] = positionals(subArgs);
|
|
@@ -2174,13 +2840,13 @@ async function handlePlanner(subArgs, injectedApi) {
|
|
|
2174
2840
|
const settings = await persistEnv(api, { AI_PROVIDER: provider });
|
|
2175
2841
|
const recall2 = describePlannerSwitch(settings, provider);
|
|
2176
2842
|
writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
|
|
2177
|
-
const meta =
|
|
2843
|
+
const meta = import_core13.ALL_PROVIDERS[provider];
|
|
2178
2844
|
const catalog = await fetchCatalog(api);
|
|
2179
2845
|
console.log(
|
|
2180
|
-
(0,
|
|
2846
|
+
(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}.`
|
|
2181
2847
|
);
|
|
2182
2848
|
console.log(plannerModelLine(recall2));
|
|
2183
|
-
if (!(0,
|
|
2849
|
+
if (!(0, import_core13.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
|
|
2184
2850
|
console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
|
|
2185
2851
|
}
|
|
2186
2852
|
}
|
|
@@ -2198,15 +2864,15 @@ async function showPlanners(api) {
|
|
|
2198
2864
|
const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
|
|
2199
2865
|
const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
2200
2866
|
console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
|
|
2201
|
-
for (const id of
|
|
2202
|
-
const runner = (0,
|
|
2867
|
+
for (const id of import_core13.CLI_PROVIDERS) {
|
|
2868
|
+
const runner = (0, import_core13.runnerForProvider)(id);
|
|
2203
2869
|
const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
|
|
2204
|
-
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${
|
|
2870
|
+
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core13.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
|
|
2205
2871
|
}
|
|
2206
2872
|
console.log("\nAPI providers");
|
|
2207
|
-
for (const id of
|
|
2873
|
+
for (const id of import_core13.PROVIDER_PRIORITY) {
|
|
2208
2874
|
const configured = catalog.providers.includes(id);
|
|
2209
|
-
const meta =
|
|
2875
|
+
const meta = import_core13.ALL_PROVIDERS[id];
|
|
2210
2876
|
console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
|
|
2211
2877
|
}
|
|
2212
2878
|
console.log(`
|
|
@@ -2223,7 +2889,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
|
|
|
2223
2889
|
const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
|
|
2224
2890
|
const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
|
|
2225
2891
|
const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
|
|
2226
|
-
const runner = (0,
|
|
2892
|
+
const runner = (0, import_core13.runnerForProvider)(provider);
|
|
2227
2893
|
if (!runner) {
|
|
2228
2894
|
fail(
|
|
2229
2895
|
"Thinking effort applies to a coding-agent planner.",
|
|
@@ -2263,29 +2929,29 @@ async function handlePlannerEffort(subArgs, injectedApi) {
|
|
|
2263
2929
|
}
|
|
2264
2930
|
|
|
2265
2931
|
// src/commands/key.ts
|
|
2266
|
-
var
|
|
2267
|
-
var
|
|
2932
|
+
var import_core14 = require("@ordewell/core");
|
|
2933
|
+
var USAGE5 = "Usage: ordewell key [list | set <provider> <key>]";
|
|
2268
2934
|
async function handleKey(subArgs, injectedApi) {
|
|
2269
2935
|
const [action, provider, ...keyParts] = positionals(subArgs);
|
|
2270
2936
|
if (action && action !== "list" && action !== "set") {
|
|
2271
|
-
fail(`Unknown key action: "${action}"`,
|
|
2937
|
+
fail(`Unknown key action: "${action}"`, USAGE5);
|
|
2272
2938
|
}
|
|
2273
2939
|
const api = await connect(subArgs, injectedApi);
|
|
2274
2940
|
if (action !== "set") {
|
|
2275
2941
|
const catalog = await fetchCatalog(api);
|
|
2276
2942
|
console.log("\nAPI provider keys\n");
|
|
2277
|
-
for (const id2 of
|
|
2278
|
-
const meta2 =
|
|
2943
|
+
for (const id2 of import_core14.PROVIDER_PRIORITY) {
|
|
2944
|
+
const meta2 = import_core14.ALL_PROVIDERS[id2];
|
|
2279
2945
|
const configured = catalog.providers.includes(id2);
|
|
2280
2946
|
console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
|
|
2281
2947
|
}
|
|
2282
2948
|
console.log(`
|
|
2283
|
-
\u2713 = a working key was found. ${
|
|
2949
|
+
\u2713 = a working key was found. ${USAGE5}`);
|
|
2284
2950
|
return;
|
|
2285
2951
|
}
|
|
2286
|
-
if (!provider) fail(
|
|
2952
|
+
if (!provider) fail(USAGE5);
|
|
2287
2953
|
const id = provider.toLowerCase();
|
|
2288
|
-
const meta =
|
|
2954
|
+
const meta = import_core14.ALL_PROVIDERS[id];
|
|
2289
2955
|
if (!meta) {
|
|
2290
2956
|
fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
|
|
2291
2957
|
}
|
|
@@ -2296,7 +2962,7 @@ async function handleKey(subArgs, injectedApi) {
|
|
|
2296
2962
|
}
|
|
2297
2963
|
|
|
2298
2964
|
// src/commands/runners.ts
|
|
2299
|
-
var
|
|
2965
|
+
var USAGE6 = "Usage: ordewell runners [<runner-id> on|off]";
|
|
2300
2966
|
async function handleRunners(subArgs, injectedApi) {
|
|
2301
2967
|
const [runner, toggle] = positionals(subArgs);
|
|
2302
2968
|
const api = await connect(subArgs, injectedApi);
|
|
@@ -2307,7 +2973,7 @@ async function handleRunners(subArgs, injectedApi) {
|
|
|
2307
2973
|
console.log(` ${r.enabled ? "\u2713" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (disabled)"}`);
|
|
2308
2974
|
}
|
|
2309
2975
|
console.log(`
|
|
2310
|
-
\u2713 = offered to the planner. ${
|
|
2976
|
+
\u2713 = offered to the planner. ${USAGE6}`);
|
|
2311
2977
|
return;
|
|
2312
2978
|
}
|
|
2313
2979
|
const state = await api.getRunners();
|
|
@@ -2316,7 +2982,7 @@ async function handleRunners(subArgs, injectedApi) {
|
|
|
2316
2982
|
fail(`Unknown runner: ${runner}`, `Enabled or not, this daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
|
|
2317
2983
|
}
|
|
2318
2984
|
const enabled = toggle === void 0 ? !known.enabled : toggle === "on" ? true : toggle === "off" ? false : null;
|
|
2319
|
-
if (enabled === null) fail(
|
|
2985
|
+
if (enabled === null) fail(USAGE6);
|
|
2320
2986
|
await api.setRunnerEnabled(runner, enabled);
|
|
2321
2987
|
console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
|
|
2322
2988
|
}
|
|
@@ -2352,20 +3018,21 @@ async function handleRefresh(subArgs, injectedApi) {
|
|
|
2352
3018
|
async function handleApprove(subArgs, injectedApi) {
|
|
2353
3019
|
const sessionId = resolveSessionId(subArgs);
|
|
2354
3020
|
const api = await connect(subArgs, injectedApi);
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
3021
|
+
await followExecution(api, sessionId, async () => {
|
|
3022
|
+
try {
|
|
3023
|
+
await api.approveReview(sessionId);
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
console.error(`Failed to approve: ${err.message}`);
|
|
3026
|
+
process.exit(1);
|
|
3027
|
+
}
|
|
3028
|
+
console.error("Plan approved \u2014 continuing execution.");
|
|
3029
|
+
});
|
|
2363
3030
|
}
|
|
2364
3031
|
|
|
2365
3032
|
// src/tui/terminalLauncher.ts
|
|
2366
3033
|
var import_child_process3 = require("child_process");
|
|
2367
3034
|
var import_util = require("util");
|
|
2368
|
-
var
|
|
3035
|
+
var import_core15 = require("@ordewell/core");
|
|
2369
3036
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
2370
3037
|
var defaultExecFile = async (command, args) => {
|
|
2371
3038
|
const { stdout, stderr } = await execFileAsync(command, args);
|
|
@@ -2385,16 +3052,16 @@ var defaultWhich = (bin) => {
|
|
|
2385
3052
|
async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
|
|
2386
3053
|
const platform = deps.platform ?? process.platform;
|
|
2387
3054
|
const env = deps.env ?? process.env;
|
|
2388
|
-
const hasTmuxImpl = deps.hasTmuxImpl ??
|
|
3055
|
+
const hasTmuxImpl = deps.hasTmuxImpl ?? import_core15.hasTmux;
|
|
2389
3056
|
const execFileImpl = deps.execFileImpl ?? defaultExecFile;
|
|
2390
3057
|
const spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
2391
3058
|
const which = deps.which ?? defaultWhich;
|
|
2392
3059
|
if (!hasTmuxImpl()) {
|
|
2393
3060
|
return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
|
|
2394
3061
|
}
|
|
2395
|
-
const session = (0,
|
|
2396
|
-
const socket = (0,
|
|
2397
|
-
const base = (0,
|
|
3062
|
+
const session = (0, import_core15.tmuxSessionName)(port);
|
|
3063
|
+
const socket = (0, import_core15.tmuxSocketName)(port);
|
|
3064
|
+
const base = (0, import_core15.tmuxWindowName)(taskId, planSessionId);
|
|
2398
3065
|
let windows;
|
|
2399
3066
|
try {
|
|
2400
3067
|
const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
|
|
@@ -2688,149 +3355,7 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
2688
3355
|
}
|
|
2689
3356
|
|
|
2690
3357
|
// src/tui/index.ts
|
|
2691
|
-
var
|
|
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];
|
|
2793
|
-
}
|
|
2794
|
-
const candidate = first ? piece : `${current} ${piece}`;
|
|
2795
|
-
const candidateStart = first ? pieceStart : currentStart;
|
|
2796
|
-
first = false;
|
|
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;
|
|
2804
|
-
}
|
|
2805
|
-
wordPos += word.length + 1;
|
|
2806
|
-
}
|
|
2807
|
-
out.push({ line: current, start: currentStart });
|
|
2808
|
-
pos += paragraph.length + 1;
|
|
2809
|
-
}
|
|
2810
|
-
return out.length > 0 ? out : [{ line: "", start: 0 }];
|
|
2811
|
-
}
|
|
2812
|
-
function wrap(text, max) {
|
|
2813
|
-
return wrapLines(text, max).map((l) => l.line);
|
|
2814
|
-
}
|
|
2815
|
-
function colour(open) {
|
|
2816
|
-
return (text) => style.enabled ? `\x1B[${open}m${text}\x1B[0m` : text;
|
|
2817
|
-
}
|
|
2818
|
-
var style = {
|
|
2819
|
-
/** Set false when the output is piped or the terminal has no colour. */
|
|
2820
|
-
enabled: true,
|
|
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
|
-
};
|
|
3358
|
+
var import_core19 = require("@ordewell/core");
|
|
2834
3359
|
|
|
2835
3360
|
// src/tui/editor.ts
|
|
2836
3361
|
function emptyEditor() {
|
|
@@ -2968,6 +3493,7 @@ var SLASH_COMMANDS = [
|
|
|
2968
3493
|
{ name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
|
|
2969
3494
|
{ name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
|
|
2970
3495
|
{ name: "stop", usage: "/stop", description: "Stop the planner or the running execution, whichever is active", category: "planning" },
|
|
3496
|
+
{ 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
3497
|
// Tasks
|
|
2972
3498
|
{ name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
|
|
2973
3499
|
{ name: "remove-task", usage: "/remove-task <id>", description: "Remove a task from the plan", category: "tasks" },
|
|
@@ -2997,6 +3523,9 @@ var SLASH_COMMANDS = [
|
|
|
2997
3523
|
{ name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
|
|
2998
3524
|
// Sessions
|
|
2999
3525
|
{ name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
|
|
3526
|
+
{ name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
|
|
3527
|
+
{ name: "compact", usage: "/compact", description: "Condense this conversation into a summary; the last two exchanges and all tasks are kept", category: "session" },
|
|
3528
|
+
{ name: "rewind", usage: "/rewind [<message>]", description: "Rewind the conversation to just before one of your messages (tasks are kept)", category: "session" },
|
|
3000
3529
|
{ name: "new", usage: "/new", description: "Start a new session", category: "session" },
|
|
3001
3530
|
{ name: "save", usage: "/save", description: "Save the current session", category: "session" },
|
|
3002
3531
|
{ name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
|
|
@@ -3268,6 +3797,7 @@ function initialState(overrides = {}) {
|
|
|
3268
3797
|
skills: noSkills(),
|
|
3269
3798
|
runners: [],
|
|
3270
3799
|
sessions: [],
|
|
3800
|
+
rewindTargets: null,
|
|
3271
3801
|
models: [],
|
|
3272
3802
|
modesByRunner: {},
|
|
3273
3803
|
orchestratorModels: [],
|
|
@@ -3281,6 +3811,7 @@ function initialState(overrides = {}) {
|
|
|
3281
3811
|
mouseCapture: true,
|
|
3282
3812
|
selection: null,
|
|
3283
3813
|
workspace: process.cwd(),
|
|
3814
|
+
handoff: null,
|
|
3284
3815
|
overlay: null,
|
|
3285
3816
|
pendingApprovals: [],
|
|
3286
3817
|
toast: "",
|
|
@@ -3293,7 +3824,7 @@ function initialState(overrides = {}) {
|
|
|
3293
3824
|
}
|
|
3294
3825
|
|
|
3295
3826
|
// src/tui/layout.ts
|
|
3296
|
-
var
|
|
3827
|
+
var import_core16 = require("@ordewell/core");
|
|
3297
3828
|
var CHROME_ROWS = 4;
|
|
3298
3829
|
function chatInputWrap(state) {
|
|
3299
3830
|
const active = state.focus === "chat" && !state.overlay;
|
|
@@ -3301,7 +3832,9 @@ function chatInputWrap(state) {
|
|
|
3301
3832
|
return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
|
|
3302
3833
|
}
|
|
3303
3834
|
function footerHints(state) {
|
|
3304
|
-
const
|
|
3835
|
+
const selected = selectedPlanRow(state)?.task;
|
|
3836
|
+
const markHint = selected?.status === "completed" ? "m undone" : "m done";
|
|
3837
|
+
const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
|
|
3305
3838
|
const planning = state.status === "planning" || state.status === "researching";
|
|
3306
3839
|
const escHint = planning ? "esc stop planning" : null;
|
|
3307
3840
|
if (state.focus === "plan") {
|
|
@@ -3324,6 +3857,7 @@ function footerHints(state) {
|
|
|
3324
3857
|
"a add",
|
|
3325
3858
|
"d remove",
|
|
3326
3859
|
"t terminal",
|
|
3860
|
+
...resolveHint,
|
|
3327
3861
|
"pgup/pgdn scroll",
|
|
3328
3862
|
"tab chat"
|
|
3329
3863
|
];
|
|
@@ -3429,9 +3963,9 @@ function chatScrollMax(state) {
|
|
|
3429
3963
|
}
|
|
3430
3964
|
function plannerSetup(state) {
|
|
3431
3965
|
const provider = state.plannerProvider;
|
|
3432
|
-
const registration =
|
|
3966
|
+
const registration = import_core16.ALL_PROVIDERS[provider];
|
|
3433
3967
|
if (!registration) return { label: "not set", ready: false };
|
|
3434
|
-
const runner = (0,
|
|
3968
|
+
const runner = (0, import_core16.runnerForProvider)(provider);
|
|
3435
3969
|
const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
|
|
3436
3970
|
return { label: registration.label, ready };
|
|
3437
3971
|
}
|
|
@@ -3578,7 +4112,7 @@ function taskLines(state, row, index, cols) {
|
|
|
3578
4112
|
const icon = paint(rawIcon);
|
|
3579
4113
|
const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
|
|
3580
4114
|
const caret = selected ? style.cyan("\u276F") : " ";
|
|
3581
|
-
const orderLabel = parent ? (0,
|
|
4115
|
+
const orderLabel = parent ? (0, import_core16.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
|
|
3582
4116
|
const indent = parent ? " " : "";
|
|
3583
4117
|
const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
|
|
3584
4118
|
const titleRoom = Math.max(1, cols - width(head));
|
|
@@ -3602,9 +4136,21 @@ function taskLines(state, row, index, cols) {
|
|
|
3602
4136
|
const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
|
|
3603
4137
|
if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
|
|
3604
4138
|
if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
|
|
4139
|
+
if (task.isolation?.state === "conflict") {
|
|
4140
|
+
const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
|
|
4141
|
+
lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where} \u2014 its work is kept on its own branch`, cols)));
|
|
4142
|
+
}
|
|
3605
4143
|
let editorLine;
|
|
3606
4144
|
if (expanded) {
|
|
3607
4145
|
lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
|
|
4146
|
+
if (task.isolation && task.isolation.state !== "none" && task.isolation.branch) {
|
|
4147
|
+
lines.push(...taskText("Branch", task.isolation.branch, cols, bodyPad));
|
|
4148
|
+
const repos = taskRepoNames(task.isolation);
|
|
4149
|
+
if (repos.length > 0) lines.push(...taskText("Repos", repos.join(", "), cols, bodyPad));
|
|
4150
|
+
if (task.isolation.state !== "integrated" && task.isolation.worktree) {
|
|
4151
|
+
lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
3608
4154
|
if (modeInfo?.autonomous) {
|
|
3609
4155
|
lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
|
|
3610
4156
|
}
|
|
@@ -3934,8 +4480,51 @@ function renderOverlay(state, rows, cols) {
|
|
|
3934
4480
|
cols
|
|
3935
4481
|
);
|
|
3936
4482
|
}
|
|
4483
|
+
if (overlay.kind === "handoff") return renderHandoff(state, overlay, rows, cols);
|
|
3937
4484
|
return renderPicker(overlay.picker, rows, cols);
|
|
3938
4485
|
}
|
|
4486
|
+
function renderHandoff(state, overlay, rows, cols) {
|
|
4487
|
+
const handoff = state.handoff;
|
|
4488
|
+
if (!handoff) return frame("Handoff", [style.grey("Nothing to hand off.")], rows, cols);
|
|
4489
|
+
if (overlay.diff) {
|
|
4490
|
+
const room = diffRoom(rows);
|
|
4491
|
+
const { lines, scroll } = overlay.diff;
|
|
4492
|
+
const shown = lines.slice(scroll, scroll + room).map((line) => paintDiffLine(truncate(line, Math.max(1, cols - 2))));
|
|
4493
|
+
const more = scroll + room < lines.length ? "\u2191\u2193 pgup/pgdn scroll \xB7 " : "";
|
|
4494
|
+
return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
|
|
4495
|
+
}
|
|
4496
|
+
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => ` ${style.green("\u2713")} #${task.order} ${task.title}`);
|
|
4497
|
+
const actions = handoffActions(handoff).map((action, index) => {
|
|
4498
|
+
const active = index === overlay.index;
|
|
4499
|
+
const caret = active ? style.cyan("\u276F") : " ";
|
|
4500
|
+
return `${caret} ${active ? style.bold(action.label) : action.label}${style.grey(` \u2014 ${action.hint}`)}`;
|
|
4501
|
+
});
|
|
4502
|
+
const perRepo = isRepoGroup(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
|
|
4503
|
+
return frame(
|
|
4504
|
+
"Run handoff",
|
|
4505
|
+
[
|
|
4506
|
+
`Branch ${style.cyan(handoffBranch(handoff))}`,
|
|
4507
|
+
style.grey(`Forked from ${handoffBase(handoff, 12)}`),
|
|
4508
|
+
"",
|
|
4509
|
+
...perRepo,
|
|
4510
|
+
"Landed on it, in plan order:",
|
|
4511
|
+
...landed,
|
|
4512
|
+
"",
|
|
4513
|
+
...actions,
|
|
4514
|
+
"",
|
|
4515
|
+
style.grey("\u2191\u2193 choose \xB7 enter selects \xB7 esc closes")
|
|
4516
|
+
],
|
|
4517
|
+
rows,
|
|
4518
|
+
cols
|
|
4519
|
+
);
|
|
4520
|
+
}
|
|
4521
|
+
function paintDiffLine(line) {
|
|
4522
|
+
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff ")) return style.bold(line);
|
|
4523
|
+
if (line.startsWith("+")) return style.green(line);
|
|
4524
|
+
if (line.startsWith("-")) return style.red(line);
|
|
4525
|
+
if (line.startsWith("@@")) return style.cyan(line);
|
|
4526
|
+
return line;
|
|
4527
|
+
}
|
|
3939
4528
|
function renderPicker(picker2, rows, cols) {
|
|
3940
4529
|
const items = visibleItems(picker2);
|
|
3941
4530
|
const body = [];
|
|
@@ -3974,26 +4563,16 @@ function frame(title, body, rows, cols) {
|
|
|
3974
4563
|
}
|
|
3975
4564
|
|
|
3976
4565
|
// src/tui/reducer.ts
|
|
3977
|
-
var
|
|
4566
|
+
var import_core17 = require("@ordewell/core");
|
|
3978
4567
|
var import_plan_utils3 = require("@ordewell/core/plan-utils");
|
|
3979
|
-
var KNOWN_PROVIDERS2 = Object.keys(
|
|
4568
|
+
var KNOWN_PROVIDERS2 = Object.keys(import_core17.ALL_PROVIDERS);
|
|
3980
4569
|
var DEFAULT_EFFORT = "__runner_default__";
|
|
3981
4570
|
function step(state, effects = []) {
|
|
3982
4571
|
return { state, effects };
|
|
3983
4572
|
}
|
|
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
4573
|
function restoredMessages(history) {
|
|
3995
4574
|
return history.map((entry) => ({
|
|
3996
|
-
role: entry.kind === "plan_generated"
|
|
4575
|
+
role: entry.kind === "plan_generated" || entry.kind === "system" || entry.kind === "compaction" ? "system" : entry.role,
|
|
3997
4576
|
content: sanitize(entry.kind === "plan_generated" ? "Plan generated." : entry.content),
|
|
3998
4577
|
timestamp: entry.timestamp
|
|
3999
4578
|
}));
|
|
@@ -4001,7 +4580,9 @@ function restoredMessages(history) {
|
|
|
4001
4580
|
var isPendingResearch = (m) => m.role === "research" && !m.research?.outcome;
|
|
4002
4581
|
function alreadySpoken(messages, content) {
|
|
4003
4582
|
const last = findLastIndex(messages, (m) => m.role === "assistant" || m.role === "user");
|
|
4004
|
-
|
|
4583
|
+
const spoken = last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
|
|
4584
|
+
const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
|
|
4585
|
+
return spoken || redrawn;
|
|
4005
4586
|
}
|
|
4006
4587
|
function researchLabel(messages, summary) {
|
|
4007
4588
|
const others = messages.filter(isPendingResearch).length - 1;
|
|
@@ -4025,20 +4606,22 @@ function findLastIndex(items, match) {
|
|
|
4025
4606
|
return -1;
|
|
4026
4607
|
}
|
|
4027
4608
|
var THINKING_TAIL = 400;
|
|
4028
|
-
var
|
|
4609
|
+
var fail3 = (state, message) => step(say(state, "error", message));
|
|
4029
4610
|
var stale = (state, sessionId) => sessionId !== void 0 && sessionId !== state.sessionId;
|
|
4030
4611
|
function reduce(state, action) {
|
|
4031
4612
|
switch (action.type) {
|
|
4032
4613
|
case "key":
|
|
4033
4614
|
return handleKey2(state, action.key);
|
|
4615
|
+
// Isolation belongs to one session's plan, and task ids repeat across
|
|
4616
|
+
// sessions, so nothing of the last one's may carry over into this.
|
|
4034
4617
|
case "sessionStarted":
|
|
4035
|
-
return step({ ...state, sessionId: action.sessionId, goal: action.goal });
|
|
4618
|
+
return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
|
|
4036
4619
|
// The daemon registers a session only once planning succeeds; when it does
|
|
4037
4620
|
// not, holding on to the id would send every next message to a session the
|
|
4038
4621
|
// server never had.
|
|
4039
4622
|
case "sessionCleared":
|
|
4040
4623
|
return step({
|
|
4041
|
-
...state,
|
|
4624
|
+
...clearIsolation(state),
|
|
4042
4625
|
sessionId: null,
|
|
4043
4626
|
goal: "",
|
|
4044
4627
|
pendingApprovals: [],
|
|
@@ -4057,11 +4640,13 @@ function reduce(state, action) {
|
|
|
4057
4640
|
return step({ ...state, messages: restoredMessages(action.history), scroll: 0 });
|
|
4058
4641
|
case "planUpdated": {
|
|
4059
4642
|
if (stale(state, action.sessionId)) return step(state);
|
|
4060
|
-
const
|
|
4643
|
+
const isolation = isolationOfPlan(action.plan);
|
|
4644
|
+
const tasks = isolationForPlan(normalizeTasks(action.plan), isolation, state.tasks);
|
|
4061
4645
|
const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
|
|
4062
4646
|
const next = {
|
|
4063
4647
|
...state,
|
|
4064
4648
|
tasks,
|
|
4649
|
+
handoff: isolation ? isolation.handoff : state.handoff,
|
|
4065
4650
|
status: state.status === "planning" || state.status === "researching" ? "idle" : state.status,
|
|
4066
4651
|
busyLabel: "",
|
|
4067
4652
|
thinkingLine: "",
|
|
@@ -4133,9 +4718,10 @@ function reduce(state, action) {
|
|
|
4133
4718
|
const update = updates[t.id];
|
|
4134
4719
|
if (update === void 0) return t;
|
|
4135
4720
|
const idleSince = update.idleSince ?? null;
|
|
4136
|
-
|
|
4721
|
+
const isolation = update.isolation ?? t.isolation;
|
|
4722
|
+
if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
|
|
4137
4723
|
changed = true;
|
|
4138
|
-
return { ...t, status: update.status, idleSince };
|
|
4724
|
+
return { ...t, status: update.status, idleSince, isolation };
|
|
4139
4725
|
}
|
|
4140
4726
|
return t;
|
|
4141
4727
|
});
|
|
@@ -4148,6 +4734,23 @@ function reduce(state, action) {
|
|
|
4148
4734
|
if (sessionId === null) return step(state);
|
|
4149
4735
|
return step(state, [{ type: "processQueued", sessionId }]);
|
|
4150
4736
|
}
|
|
4737
|
+
case "isolationBlocked": {
|
|
4738
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4739
|
+
return step(blockedPicker(state, action.message, action.repos));
|
|
4740
|
+
}
|
|
4741
|
+
case "isolationHandoff": {
|
|
4742
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4743
|
+
return step(handoffArrived(state, action.handoff));
|
|
4744
|
+
}
|
|
4745
|
+
case "handoffDiff": {
|
|
4746
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4747
|
+
return step(showDiff(state, action.diff));
|
|
4748
|
+
}
|
|
4749
|
+
case "handoffDiscarded": {
|
|
4750
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4751
|
+
const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
|
|
4752
|
+
return step({ ...clearIsolation(state), overlay });
|
|
4753
|
+
}
|
|
4151
4754
|
case "executionComplete": {
|
|
4152
4755
|
if (stale(state, action.sessionId)) return step(state);
|
|
4153
4756
|
const { completed, total, failed } = action.summary ?? {
|
|
@@ -4182,6 +4785,23 @@ function reduce(state, action) {
|
|
|
4182
4785
|
);
|
|
4183
4786
|
case "sessionsLoaded":
|
|
4184
4787
|
return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
|
|
4788
|
+
case "rewindTargetsLoaded":
|
|
4789
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4790
|
+
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
4791
|
+
// A fork holds no run, whatever the session it came from was doing — the
|
|
4792
|
+
// original keeps its run, and its events are stale from here on.
|
|
4793
|
+
case "sessionForked":
|
|
4794
|
+
return step({
|
|
4795
|
+
...clearIsolation(state),
|
|
4796
|
+
sessionId: action.sessionId,
|
|
4797
|
+
goal: action.goal,
|
|
4798
|
+
status: "idle",
|
|
4799
|
+
busyLabel: "",
|
|
4800
|
+
thinkingLine: "",
|
|
4801
|
+
planApproved: false,
|
|
4802
|
+
pendingApprovals: [],
|
|
4803
|
+
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
4804
|
+
});
|
|
4185
4805
|
case "runnersLoaded":
|
|
4186
4806
|
return step(refillPicker({
|
|
4187
4807
|
...state,
|
|
@@ -4286,11 +4906,11 @@ function modelDetail(m) {
|
|
|
4286
4906
|
function providerErrorHint(state) {
|
|
4287
4907
|
const failed = Object.keys(state.providerErrors ?? {});
|
|
4288
4908
|
if (failed.length === 0) return void 0;
|
|
4289
|
-
const names = failed.map((p) =>
|
|
4909
|
+
const names = failed.map((p) => import_core17.ALL_PROVIDERS[p]?.label ?? p);
|
|
4290
4910
|
return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
|
|
4291
4911
|
}
|
|
4292
4912
|
function plannerModelItems(state) {
|
|
4293
|
-
const runner = (0,
|
|
4913
|
+
const runner = (0, import_core17.runnerForProvider)(state.plannerProvider);
|
|
4294
4914
|
if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
|
|
4295
4915
|
const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
|
|
4296
4916
|
id: m.id,
|
|
@@ -4304,7 +4924,7 @@ function plannerModelItems(state) {
|
|
|
4304
4924
|
return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
|
|
4305
4925
|
}
|
|
4306
4926
|
function plannerEffortItems(state) {
|
|
4307
|
-
const runner = (0,
|
|
4927
|
+
const runner = (0, import_core17.runnerForProvider)(state.plannerProvider);
|
|
4308
4928
|
if (!runner) {
|
|
4309
4929
|
return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
|
|
4310
4930
|
}
|
|
@@ -4331,6 +4951,13 @@ function pickerItemsFor(state, action) {
|
|
|
4331
4951
|
if (action.kind === "set-planner-effort") {
|
|
4332
4952
|
return plannerEffortItems(state);
|
|
4333
4953
|
}
|
|
4954
|
+
if (action.kind === "rewind") {
|
|
4955
|
+
if (state.rewindTargets === null) return [];
|
|
4956
|
+
if (state.rewindTargets.length === 0) {
|
|
4957
|
+
return [{ id: "", label: "Nothing to rewind to yet", detail: "the only message so far is the goal", disabled: true }];
|
|
4958
|
+
}
|
|
4959
|
+
return [...state.rewindTargets].reverse().map((t) => ({ id: String(t.index), label: t.preview, detail: `message ${t.index}` }));
|
|
4960
|
+
}
|
|
4334
4961
|
if (action.kind === "load-session" || action.kind === "delete-session") {
|
|
4335
4962
|
return state.sessions.map((s) => ({
|
|
4336
4963
|
id: s.id,
|
|
@@ -4561,6 +5188,9 @@ function handlePlanKey(state, key) {
|
|
|
4561
5188
|
if (key.char === "t") {
|
|
4562
5189
|
return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
|
|
4563
5190
|
}
|
|
5191
|
+
if (key.char === "x" && task.isolation?.state === "conflict") {
|
|
5192
|
+
return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
|
|
5193
|
+
}
|
|
4564
5194
|
if (key.char === "R") return openTaskRunnerPicker(state, task);
|
|
4565
5195
|
if (key.char === "o") return openTaskModelPicker(state, task);
|
|
4566
5196
|
if (key.char === "e") return openTaskEffortPicker(state, task);
|
|
@@ -4618,9 +5248,13 @@ function handleApprovalKey(state, overlay, key) {
|
|
|
4618
5248
|
function handleOverlayKey(state, overlay, key) {
|
|
4619
5249
|
if (overlay.kind === "help") return handleHelpKey(state, overlay.scroll ?? 0, key);
|
|
4620
5250
|
if (key.name === "wheelignored") return step(state);
|
|
5251
|
+
if (overlay.kind === "handoff") return handleHandoffKey(state, overlay, key, bodyRows(state));
|
|
4621
5252
|
if (isWheel(key) && overlay.kind !== "picker") return scrollPointed(state, key);
|
|
4622
5253
|
if (overlay.kind === "approval") return handleApprovalKey(state, overlay, key);
|
|
4623
5254
|
if (overlay.kind === "confirm") return handleConfirmKey(state, overlay, key);
|
|
5255
|
+
if (key.name === "escape" && overlay.kind === "picker" && overlay.picker.action.kind === "isolation-blocked") {
|
|
5256
|
+
return chooseBlocked(state, "cancel");
|
|
5257
|
+
}
|
|
4624
5258
|
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
4625
5259
|
if (overlay.kind === "prompt") return handlePromptKey(state, overlay, key);
|
|
4626
5260
|
return handlePickerKey(state, overlay.picker, key);
|
|
@@ -4634,6 +5268,9 @@ function handleConfirmKey(state, overlay, key) {
|
|
|
4634
5268
|
if (!state.sessionId) return step(closed);
|
|
4635
5269
|
return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
|
|
4636
5270
|
}
|
|
5271
|
+
if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
|
|
5272
|
+
return confirmedHandoff(state, overlay.action.kind);
|
|
5273
|
+
}
|
|
4637
5274
|
if (overlay.action.kind === "init-workspace") {
|
|
4638
5275
|
return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
|
|
4639
5276
|
}
|
|
@@ -4661,7 +5298,7 @@ function handlePromptKey(state, overlay, key) {
|
|
|
4661
5298
|
if (overlay.action.kind === "api-key") {
|
|
4662
5299
|
return step(closed, [{ type: "setApiKey", provider: overlay.action.provider, key: value2 }]);
|
|
4663
5300
|
}
|
|
4664
|
-
if (!state.sessionId) return
|
|
5301
|
+
if (!state.sessionId) return fail3(closed, "No active plan to add a task to.");
|
|
4665
5302
|
return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
|
|
4666
5303
|
}
|
|
4667
5304
|
const value = editText(overlay.value, key);
|
|
@@ -4694,6 +5331,7 @@ function handlePickerKey(state, picker2, key) {
|
|
|
4694
5331
|
}
|
|
4695
5332
|
function choose(state, picker2, item) {
|
|
4696
5333
|
const closed = { ...state, overlay: null };
|
|
5334
|
+
if (picker2.action.kind === "isolation-blocked") return chooseBlocked(state, item?.id ?? "cancel");
|
|
4697
5335
|
if (picker2.action.kind === "set-allowlist") {
|
|
4698
5336
|
return step(closed, [{ type: "setAllowlist", runner: picker2.action.runner, modelIds: picker2.chosen }]);
|
|
4699
5337
|
}
|
|
@@ -4732,6 +5370,11 @@ function choose(state, picker2, item) {
|
|
|
4732
5370
|
return step(closed, [{ type: "setPlannerEffort", effort: item.id === DEFAULT_EFFORT ? "" : item.id }]);
|
|
4733
5371
|
case "load-session":
|
|
4734
5372
|
return step(closed, [{ type: "loadSession", sessionId: item.id }]);
|
|
5373
|
+
case "rewind":
|
|
5374
|
+
return withSession(
|
|
5375
|
+
closed,
|
|
5376
|
+
(sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index: Number(item.id) }])
|
|
5377
|
+
);
|
|
4735
5378
|
case "delete-session":
|
|
4736
5379
|
return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
|
|
4737
5380
|
case "set-key":
|
|
@@ -4801,7 +5444,7 @@ function submit(state) {
|
|
|
4801
5444
|
}
|
|
4802
5445
|
function runCommand(state, { name, args }) {
|
|
4803
5446
|
if (!findCommand(name)) {
|
|
4804
|
-
return
|
|
5447
|
+
return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
|
|
4805
5448
|
}
|
|
4806
5449
|
if (SKILL_IDS.includes(name)) {
|
|
4807
5450
|
return toggleSkill(state, name, args[0]);
|
|
@@ -4848,14 +5491,25 @@ function runCommand(state, { name, args }) {
|
|
|
4848
5491
|
{ ...state, overlay: { kind: "picker", picker: picker("Sessions", [], { kind: "load-session" }) } },
|
|
4849
5492
|
[{ type: "loadSessions" }]
|
|
4850
5493
|
);
|
|
5494
|
+
case "handoff":
|
|
5495
|
+
return handoffCommand(state, args[0]);
|
|
5496
|
+
case "fork":
|
|
5497
|
+
return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
|
|
5498
|
+
case "rewind":
|
|
5499
|
+
return rewind(state, args[0]);
|
|
5500
|
+
case "compact":
|
|
5501
|
+
return withIdlePlanner(
|
|
5502
|
+
state,
|
|
5503
|
+
(sessionId) => step({ ...state, status: "planning", busyLabel: "Condensing the conversation\u2026" }, [{ type: "compactConversation", sessionId }])
|
|
5504
|
+
);
|
|
4851
5505
|
case "new":
|
|
4852
5506
|
return requestNewSession(state);
|
|
4853
5507
|
case "save":
|
|
4854
5508
|
return withSession(state, (sessionId) => step(state, [{ type: "saveSession", sessionId }]));
|
|
4855
5509
|
case "load":
|
|
4856
|
-
return args[0] ? step(state, [{ type: "loadSession", sessionId: args[0] }]) :
|
|
5510
|
+
return args[0] ? step(state, [{ type: "loadSession", sessionId: args[0] }]) : fail3(state, "Usage: /load <session-id> \u2014 or run /sessions to pick one.");
|
|
4857
5511
|
case "delete":
|
|
4858
|
-
return args[0] ? step(state, [{ type: "deleteSession", sessionId: args[0] }]) :
|
|
5512
|
+
return args[0] ? step(state, [{ type: "deleteSession", sessionId: args[0] }]) : fail3(state, "Usage: /delete <session-id>");
|
|
4859
5513
|
case "add-task":
|
|
4860
5514
|
return addTask(state, args.join(" "));
|
|
4861
5515
|
case "remove-task":
|
|
@@ -4896,17 +5550,35 @@ function runCommand(state, { name, args }) {
|
|
|
4896
5550
|
(sessionId, taskId) => step(state, [taskActionEffect(state, sessionId, taskId, name)])
|
|
4897
5551
|
);
|
|
4898
5552
|
}
|
|
4899
|
-
return
|
|
5553
|
+
return fail3(state, `/${name} is not wired up yet.`);
|
|
4900
5554
|
}
|
|
4901
5555
|
function picker(title, items, action, extra = {}) {
|
|
4902
5556
|
return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
|
|
4903
5557
|
}
|
|
4904
5558
|
function withSession(state, run) {
|
|
4905
5559
|
if (!state.sessionId) {
|
|
4906
|
-
return
|
|
5560
|
+
return fail3(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
|
|
4907
5561
|
}
|
|
4908
5562
|
return run(state.sessionId);
|
|
4909
5563
|
}
|
|
5564
|
+
function withIdlePlanner(state, run) {
|
|
5565
|
+
return withSession(
|
|
5566
|
+
state,
|
|
5567
|
+
(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)
|
|
5568
|
+
);
|
|
5569
|
+
}
|
|
5570
|
+
function rewind(state, arg) {
|
|
5571
|
+
return withIdlePlanner(state, (sessionId) => {
|
|
5572
|
+
if (arg === void 0) {
|
|
5573
|
+
return step(
|
|
5574
|
+
{ ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "The chosen message and everything after it are discarded; the tasks stay as they are." }) } },
|
|
5575
|
+
[{ type: "loadRewindTargets", sessionId }]
|
|
5576
|
+
);
|
|
5577
|
+
}
|
|
5578
|
+
if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
|
|
5579
|
+
return step(state, [{ type: "rewindConversation", sessionId, index: Number(arg) }]);
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
4910
5582
|
function resolveToggle(arg, current) {
|
|
4911
5583
|
const value = arg?.toLowerCase();
|
|
4912
5584
|
if (value === "on") return true;
|
|
@@ -4916,7 +5588,7 @@ function resolveToggle(arg, current) {
|
|
|
4916
5588
|
}
|
|
4917
5589
|
function toggleSkill(state, skill, arg) {
|
|
4918
5590
|
const enabled = resolveToggle(arg, state.skills[skill]);
|
|
4919
|
-
if (enabled === null) return
|
|
5591
|
+
if (enabled === null) return fail3(state, `Usage: /${skill} [on|off]`);
|
|
4920
5592
|
return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
|
|
4921
5593
|
}
|
|
4922
5594
|
function requestNewSession(state) {
|
|
@@ -4936,6 +5608,7 @@ function newSession(state) {
|
|
|
4936
5608
|
const closeEffects = state.sessionId ? [{ type: "closeSession", sessionId: state.sessionId }] : [];
|
|
4937
5609
|
return step({
|
|
4938
5610
|
...state,
|
|
5611
|
+
handoff: null,
|
|
4939
5612
|
sessionId: null,
|
|
4940
5613
|
goal: "",
|
|
4941
5614
|
tasks: [],
|
|
@@ -4956,14 +5629,14 @@ function newSession(state) {
|
|
|
4956
5629
|
}
|
|
4957
5630
|
function setModel(state, args) {
|
|
4958
5631
|
if (args[0] === "set" && args[1]) {
|
|
4959
|
-
const runner = (0,
|
|
5632
|
+
const runner = (0, import_core17.runnerForProvider)(state.plannerProvider);
|
|
4960
5633
|
const known = runner ? modelsForRunner(state.models, runner) : [];
|
|
4961
5634
|
if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
|
|
4962
|
-
return
|
|
5635
|
+
return fail3(state, `${args[1]} was not discovered for ${runner}.`);
|
|
4963
5636
|
}
|
|
4964
5637
|
return step(state, [{ type: "setModel", modelId: args[1] }]);
|
|
4965
5638
|
}
|
|
4966
|
-
if (args.length > 0 && args[0] !== "set") return
|
|
5639
|
+
if (args.length > 0 && args[0] !== "set") return fail3(state, "Usage: /model [set <model-id>]");
|
|
4967
5640
|
const items = pickerItemsFor(state, { kind: "set-model" });
|
|
4968
5641
|
return step(
|
|
4969
5642
|
{
|
|
@@ -4978,21 +5651,21 @@ function setModel(state, args) {
|
|
|
4978
5651
|
}
|
|
4979
5652
|
function plannerItems(state) {
|
|
4980
5653
|
const installed = new Set(state.runners.map((r) => r.id));
|
|
4981
|
-
const agents =
|
|
4982
|
-
const runner = (0,
|
|
5654
|
+
const agents = import_core17.CLI_PROVIDERS.map((id) => {
|
|
5655
|
+
const runner = (0, import_core17.runnerForProvider)(id);
|
|
4983
5656
|
const ready = installed.has(runner);
|
|
4984
5657
|
return {
|
|
4985
5658
|
id,
|
|
4986
|
-
label:
|
|
5659
|
+
label: import_core17.ALL_PROVIDERS[id].label,
|
|
4987
5660
|
detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
|
|
4988
5661
|
selected: state.plannerProvider === id,
|
|
4989
5662
|
disabled: !ready
|
|
4990
5663
|
};
|
|
4991
5664
|
});
|
|
4992
|
-
const vendors =
|
|
5665
|
+
const vendors = import_core17.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
|
|
4993
5666
|
id,
|
|
4994
|
-
label:
|
|
4995
|
-
detail:
|
|
5667
|
+
label: import_core17.ALL_PROVIDERS[id].label,
|
|
5668
|
+
detail: import_core17.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
4996
5669
|
selected: state.plannerProvider === id
|
|
4997
5670
|
}));
|
|
4998
5671
|
return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
|
|
@@ -5001,7 +5674,7 @@ function setPlanner(state, args) {
|
|
|
5001
5674
|
if (args[0]) {
|
|
5002
5675
|
const id = args[0].toLowerCase();
|
|
5003
5676
|
if (!KNOWN_PROVIDERS2.includes(id)) {
|
|
5004
|
-
return
|
|
5677
|
+
return fail3(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
|
|
5005
5678
|
}
|
|
5006
5679
|
return step(state, [{ type: "setPlanner", provider: id }]);
|
|
5007
5680
|
}
|
|
@@ -5022,7 +5695,7 @@ function setPlannerEffort(state, args) {
|
|
|
5022
5695
|
const match = items.find((i) => !i.disabled && (i.id === wanted || wanted === "default" && i.id === DEFAULT_EFFORT));
|
|
5023
5696
|
if (!match) {
|
|
5024
5697
|
const available = items.filter((i) => !i.disabled && i.id !== DEFAULT_EFFORT).map((i) => i.id);
|
|
5025
|
-
return
|
|
5698
|
+
return fail3(state, available.length > 0 ? `Unknown effort: ${args[0]}. Available: ${available.join(", ")}, default.` : items[0]?.label ?? "No effort levels available.");
|
|
5026
5699
|
}
|
|
5027
5700
|
return step(state, [{ type: "setPlannerEffort", effort: match.id === DEFAULT_EFFORT ? "" : match.id }]);
|
|
5028
5701
|
}
|
|
@@ -5040,7 +5713,7 @@ function setKey(state, args) {
|
|
|
5040
5713
|
if (args[0] === "set" && args[1]) {
|
|
5041
5714
|
const provider = args[1].toLowerCase();
|
|
5042
5715
|
if (!KNOWN_PROVIDERS2.includes(provider)) {
|
|
5043
|
-
return
|
|
5716
|
+
return fail3(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
|
|
5044
5717
|
}
|
|
5045
5718
|
if (!args[2]) {
|
|
5046
5719
|
return step({
|
|
@@ -5050,16 +5723,16 @@ function setKey(state, args) {
|
|
|
5050
5723
|
}
|
|
5051
5724
|
return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
|
|
5052
5725
|
}
|
|
5053
|
-
const items =
|
|
5726
|
+
const items = import_core17.PROVIDER_PRIORITY.map((id) => ({
|
|
5054
5727
|
id,
|
|
5055
|
-
label:
|
|
5056
|
-
detail:
|
|
5728
|
+
label: import_core17.ALL_PROVIDERS[id].label,
|
|
5729
|
+
detail: import_core17.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5057
5730
|
selected: state.configuredProviders.includes(id)
|
|
5058
5731
|
}));
|
|
5059
5732
|
return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
|
|
5060
5733
|
}
|
|
5061
5734
|
function keyPrompt(provider) {
|
|
5062
|
-
const meta =
|
|
5735
|
+
const meta = import_core17.ALL_PROVIDERS[provider];
|
|
5063
5736
|
return {
|
|
5064
5737
|
kind: "prompt",
|
|
5065
5738
|
title: `${meta.label} API key`,
|
|
@@ -5071,23 +5744,23 @@ function keyPrompt(provider) {
|
|
|
5071
5744
|
function allowlist(state, args) {
|
|
5072
5745
|
const [sub, runner, ...rest] = args;
|
|
5073
5746
|
if (sub === "set") {
|
|
5074
|
-
if (!runner || rest.length === 0) return
|
|
5747
|
+
if (!runner || rest.length === 0) return fail3(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
|
|
5075
5748
|
if (state.runners.length > 0 && !state.runners.some((r) => r.id === runner)) {
|
|
5076
|
-
return
|
|
5749
|
+
return fail3(state, `Unknown runner "${runner}".`);
|
|
5077
5750
|
}
|
|
5078
5751
|
const modelIds = rest.join(" ").split(",").map((s) => s.trim()).filter(Boolean);
|
|
5079
|
-
if (modelIds.length === 0) return
|
|
5752
|
+
if (modelIds.length === 0) return fail3(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
|
|
5080
5753
|
const known = modelsForRunner(state.models, runner);
|
|
5081
5754
|
if (state.models.length > 0 && known.length > 0) {
|
|
5082
5755
|
const stray = modelIds.filter((id) => !known.some((m) => m.id === id));
|
|
5083
5756
|
if (stray.length > 0) {
|
|
5084
|
-
return
|
|
5757
|
+
return fail3(state, `Not discovered for ${runner}: ${stray.join(", ")}.`);
|
|
5085
5758
|
}
|
|
5086
5759
|
}
|
|
5087
5760
|
return step(state, [{ type: "setAllowlist", runner, modelIds }]);
|
|
5088
5761
|
}
|
|
5089
5762
|
if (sub === "clear") {
|
|
5090
|
-
if (!runner) return
|
|
5763
|
+
if (!runner) return fail3(state, "Usage: /allowlist clear <runner>");
|
|
5091
5764
|
return step(state, [{ type: "setAllowlist", runner, modelIds: [] }]);
|
|
5092
5765
|
}
|
|
5093
5766
|
if (sub === "show" || sub === void 0) {
|
|
@@ -5109,7 +5782,7 @@ function allowlist(state, args) {
|
|
|
5109
5782
|
[{ type: "loadModels" }]
|
|
5110
5783
|
);
|
|
5111
5784
|
}
|
|
5112
|
-
return
|
|
5785
|
+
return fail3(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
|
|
5113
5786
|
}
|
|
5114
5787
|
function describeAllowlist(ids) {
|
|
5115
5788
|
if (!ids || ids.length === 0) return "no restriction";
|
|
@@ -5120,7 +5793,7 @@ function runners(state, args) {
|
|
|
5120
5793
|
if (runner) {
|
|
5121
5794
|
const known = state.runners.find((r) => r.id === runner);
|
|
5122
5795
|
const enabled = resolveToggle(arg, known?.enabled ?? true);
|
|
5123
|
-
if (enabled === null) return
|
|
5796
|
+
if (enabled === null) return fail3(state, "Usage: /runners [<runner-id> on|off]");
|
|
5124
5797
|
return step(state, [{ type: "setRunnerEnabled", runner, enabled }]);
|
|
5125
5798
|
}
|
|
5126
5799
|
const action = { kind: "set-runners" };
|
|
@@ -5136,12 +5809,12 @@ function runners(state, args) {
|
|
|
5136
5809
|
}
|
|
5137
5810
|
function setAutonomous(state, arg) {
|
|
5138
5811
|
const enabled = resolveToggle(arg, state.autonomous);
|
|
5139
|
-
if (enabled === null) return
|
|
5812
|
+
if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
|
|
5140
5813
|
return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
|
|
5141
5814
|
}
|
|
5142
5815
|
function setMouseCapture(state, arg) {
|
|
5143
5816
|
const enabled = resolveToggle(arg, state.mouseCapture);
|
|
5144
|
-
if (enabled === null) return
|
|
5817
|
+
if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
|
|
5145
5818
|
const noted = say(
|
|
5146
5819
|
// A selection made under capture is meaningless once the terminal owns the
|
|
5147
5820
|
// mouse again, and its highlight would sit on screen with nothing able to
|
|
@@ -5153,7 +5826,7 @@ function setMouseCapture(state, arg) {
|
|
|
5153
5826
|
return step(noted, [{ type: "setMouseCapture", enabled }]);
|
|
5154
5827
|
}
|
|
5155
5828
|
function openTaskRunnerPicker(state, task) {
|
|
5156
|
-
if (task.type !== "ai") return
|
|
5829
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
|
|
5157
5830
|
const action = { kind: "set-task-runner", taskId: task.id };
|
|
5158
5831
|
return step(
|
|
5159
5832
|
{
|
|
@@ -5183,7 +5856,7 @@ function confirmRemoveTask(state, task) {
|
|
|
5183
5856
|
function openTaskDepsPicker(state, task) {
|
|
5184
5857
|
const action = { kind: "set-task-deps", taskId: task.id };
|
|
5185
5858
|
const items = pickerItemsFor(state, action);
|
|
5186
|
-
if (items.length === 0) return
|
|
5859
|
+
if (items.length === 0) return fail3(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
|
|
5187
5860
|
return step({
|
|
5188
5861
|
...state,
|
|
5189
5862
|
overlay: {
|
|
@@ -5197,10 +5870,10 @@ function openTaskDepsPicker(state, task) {
|
|
|
5197
5870
|
});
|
|
5198
5871
|
}
|
|
5199
5872
|
function openTaskModePicker(state, task) {
|
|
5200
|
-
if (task.type !== "ai") return
|
|
5873
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor mode.");
|
|
5201
5874
|
const modes = modesForTask(state.modesByRunner, task);
|
|
5202
5875
|
if (modes.length === 0) {
|
|
5203
|
-
return
|
|
5876
|
+
return fail3(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
|
|
5204
5877
|
}
|
|
5205
5878
|
const action = { kind: "set-task-mode", taskId: task.id };
|
|
5206
5879
|
return step(
|
|
@@ -5216,7 +5889,7 @@ function openTaskModePicker(state, task) {
|
|
|
5216
5889
|
);
|
|
5217
5890
|
}
|
|
5218
5891
|
function openTaskModelPicker(state, task) {
|
|
5219
|
-
if (task.type !== "ai") return
|
|
5892
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor model.");
|
|
5220
5893
|
const action = { kind: "set-task-model", taskId: task.id };
|
|
5221
5894
|
return step(
|
|
5222
5895
|
{
|
|
@@ -5232,8 +5905,8 @@ function openTaskModelPicker(state, task) {
|
|
|
5232
5905
|
);
|
|
5233
5906
|
}
|
|
5234
5907
|
function openTaskEffortPicker(state, task) {
|
|
5235
|
-
if (task.type !== "ai") return
|
|
5236
|
-
if (!task.assignedModel) return
|
|
5908
|
+
if (task.type !== "ai") return fail3(state, "Manual tasks do not have a thinking effort.");
|
|
5909
|
+
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
5237
5910
|
const action = { kind: "set-task-effort", taskId: task.id };
|
|
5238
5911
|
return step(
|
|
5239
5912
|
{
|
|
@@ -5299,7 +5972,7 @@ function taskModelCommand(state, args) {
|
|
|
5299
5972
|
variants: []
|
|
5300
5973
|
};
|
|
5301
5974
|
if (!runnerAccepts(task, model)) {
|
|
5302
|
-
return
|
|
5975
|
+
return fail3(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
|
|
5303
5976
|
}
|
|
5304
5977
|
return assignTaskModel(state, sessionId, task, model);
|
|
5305
5978
|
});
|
|
@@ -5309,7 +5982,7 @@ function taskRunnerCommand(state, args) {
|
|
|
5309
5982
|
const task = findTask(state.tasks, taskId);
|
|
5310
5983
|
if (!args[1]) return openTaskRunnerPicker(state, task);
|
|
5311
5984
|
const runner = state.runners.find((candidate) => candidate.id === args[1]);
|
|
5312
|
-
if (!runner) return
|
|
5985
|
+
if (!runner) return fail3(state, `Unknown runner "${args[1]}".`);
|
|
5313
5986
|
return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
|
|
5314
5987
|
});
|
|
5315
5988
|
}
|
|
@@ -5318,7 +5991,7 @@ function taskModeCommand(state, args) {
|
|
|
5318
5991
|
const task = findTask(state.tasks, taskId);
|
|
5319
5992
|
if (!args[1]) return openTaskModePicker(state, task);
|
|
5320
5993
|
const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
|
|
5321
|
-
if (!mode) return
|
|
5994
|
+
if (!mode) return fail3(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
|
|
5322
5995
|
return assignTaskMode(state, sessionId, task, mode.id, mode.label);
|
|
5323
5996
|
});
|
|
5324
5997
|
}
|
|
@@ -5326,12 +5999,12 @@ function taskEffortCommand(state, args) {
|
|
|
5326
5999
|
return taskCommand(state, args[0], (sessionId, taskId) => {
|
|
5327
6000
|
const task = findTask(state.tasks, taskId);
|
|
5328
6001
|
if (!args[1]) return openTaskEffortPicker(state, task);
|
|
5329
|
-
if (!task.assignedModel) return
|
|
6002
|
+
if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
|
|
5330
6003
|
const effort = args[1].toLowerCase();
|
|
5331
6004
|
const value = effort === "default" || effort === "none" ? void 0 : args[1];
|
|
5332
6005
|
const supported = effortsForTask(state.models, task);
|
|
5333
6006
|
if (value && supported.length > 0 && !supported.some((variant) => variant.id === value)) {
|
|
5334
|
-
return
|
|
6007
|
+
return fail3(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
|
|
5335
6008
|
}
|
|
5336
6009
|
return assignTaskEffort(state, sessionId, task, value);
|
|
5337
6010
|
});
|
|
@@ -5349,9 +6022,9 @@ function addTask(state, title) {
|
|
|
5349
6022
|
}
|
|
5350
6023
|
function taskCommand(state, token, run) {
|
|
5351
6024
|
return withSession(state, (sessionId) => {
|
|
5352
|
-
if (!token) return
|
|
6025
|
+
if (!token) return fail3(state, "Which task? Pass a task id or its number in the plan.");
|
|
5353
6026
|
const taskId = resolveTaskId2(state, token);
|
|
5354
|
-
if (!taskId) return
|
|
6027
|
+
if (!taskId) return fail3(state, `No task matching "${token}" in the current plan.`);
|
|
5355
6028
|
return run(sessionId, taskId);
|
|
5356
6029
|
});
|
|
5357
6030
|
}
|
|
@@ -5417,7 +6090,7 @@ function createApp(deps) {
|
|
|
5417
6090
|
|
|
5418
6091
|
// src/tui/effects.ts
|
|
5419
6092
|
var import_child_process4 = require("child_process");
|
|
5420
|
-
var
|
|
6093
|
+
var import_core18 = require("@ordewell/core");
|
|
5421
6094
|
var import_plan_utils4 = require("@ordewell/core/plan-utils");
|
|
5422
6095
|
var ConversationQueue = class {
|
|
5423
6096
|
tail = Promise.resolve();
|
|
@@ -5588,7 +6261,7 @@ async function perform(effect, deps) {
|
|
|
5588
6261
|
dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
|
|
5589
6262
|
return;
|
|
5590
6263
|
case "setPlanner": {
|
|
5591
|
-
const meta =
|
|
6264
|
+
const meta = import_core18.ALL_PROVIDERS[effect.provider];
|
|
5592
6265
|
if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
|
|
5593
6266
|
const env = { AI_PROVIDER: effect.provider };
|
|
5594
6267
|
const settings = await api.updateSettings({ env });
|
|
@@ -5600,7 +6273,7 @@ async function perform(effect, deps) {
|
|
|
5600
6273
|
});
|
|
5601
6274
|
dispatch({
|
|
5602
6275
|
type: "notice",
|
|
5603
|
-
message: `${(0,
|
|
6276
|
+
message: `${(0, import_core18.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
|
|
5604
6277
|
});
|
|
5605
6278
|
await loadModels(deps);
|
|
5606
6279
|
return;
|
|
@@ -5612,7 +6285,7 @@ async function perform(effect, deps) {
|
|
|
5612
6285
|
dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
|
|
5613
6286
|
return;
|
|
5614
6287
|
case "setApiKey": {
|
|
5615
|
-
const meta =
|
|
6288
|
+
const meta = import_core18.ALL_PROVIDERS[effect.provider];
|
|
5616
6289
|
if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
|
|
5617
6290
|
await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
|
|
5618
6291
|
deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
|
|
@@ -5668,6 +6341,67 @@ async function perform(effect, deps) {
|
|
|
5668
6341
|
dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
|
|
5669
6342
|
return;
|
|
5670
6343
|
}
|
|
6344
|
+
// The daemon has already adopted the fork, so switching is only the TUI
|
|
6345
|
+
// catching up — the original session is not closed: it may be running.
|
|
6346
|
+
case "forkConversation": {
|
|
6347
|
+
const fork = await api.forkConversation(effect.sessionId);
|
|
6348
|
+
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6349
|
+
dispatch({ type: "chatRestored", history: fork.plan.conversationHistory ?? [], sessionId: fork.sessionId });
|
|
6350
|
+
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6351
|
+
dispatch({ type: "notice", message: `Forked ${effect.sessionId} into ${fork.sessionId} \u2014 you are in the fork now. /sessions to go back.` });
|
|
6352
|
+
return;
|
|
6353
|
+
}
|
|
6354
|
+
case "loadRewindTargets":
|
|
6355
|
+
dispatch({ type: "rewindTargetsLoaded", targets: await api.rewindTargets(effect.sessionId), sessionId: effect.sessionId });
|
|
6356
|
+
return;
|
|
6357
|
+
case "rewindConversation": {
|
|
6358
|
+
const plan = await api.rewindConversation(effect.sessionId, effect.index);
|
|
6359
|
+
dispatch({ type: "chatRestored", history: plan.conversationHistory ?? [], sessionId: effect.sessionId });
|
|
6360
|
+
dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
|
|
6361
|
+
dispatch({ type: "notice", message: "Rewound the conversation. The tasks are unchanged; your next message continues from here." });
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
// The redrawn transcript opens with the summary entry, which is what the
|
|
6365
|
+
// user gets to read — no separate notice to repeat it.
|
|
6366
|
+
case "compactConversation": {
|
|
6367
|
+
const { plan } = await api.compactConversation(effect.sessionId);
|
|
6368
|
+
dispatch({ type: "chatRestored", history: plan.conversationHistory ?? [], sessionId: effect.sessionId });
|
|
6369
|
+
dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
|
|
6370
|
+
return;
|
|
6371
|
+
}
|
|
6372
|
+
case "isolationReviewDiff":
|
|
6373
|
+
dispatch({ type: "handoffDiff", diff: await api.reviewRunDiff(effect.sessionId), sessionId: effect.sessionId });
|
|
6374
|
+
return;
|
|
6375
|
+
// A conflict or a refusal is an answer, not a fault: either way the user's
|
|
6376
|
+
// tree is exactly as it was, and the words say what to do next.
|
|
6377
|
+
case "isolationMerge": {
|
|
6378
|
+
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true);
|
|
6379
|
+
dispatch({ type: ok ? "notice" : "failed", message });
|
|
6380
|
+
return;
|
|
6381
|
+
}
|
|
6382
|
+
case "isolationDiscard":
|
|
6383
|
+
await api.discardRun(effect.sessionId);
|
|
6384
|
+
dispatch({ type: "handoffDiscarded", sessionId: effect.sessionId });
|
|
6385
|
+
dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
|
|
6386
|
+
return;
|
|
6387
|
+
case "isolationCleanup":
|
|
6388
|
+
await api.cleanupRun(effect.sessionId);
|
|
6389
|
+
await refreshPlan(deps, effect.sessionId);
|
|
6390
|
+
dispatch({ type: "notice", message: `Removed the run's worktrees and task branches; ${effect.branch} is kept.` });
|
|
6391
|
+
return;
|
|
6392
|
+
// Spawns runners again, so it needs the stream a run needs — the blocked
|
|
6393
|
+
// one closed itself when nothing started.
|
|
6394
|
+
case "isolationContinue": {
|
|
6395
|
+
const { sessionId } = effect;
|
|
6396
|
+
await withExecutionStream(deps, sessionId, () => effect.mode === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
|
|
6397
|
+
await refreshPlan(deps, sessionId);
|
|
6398
|
+
return;
|
|
6399
|
+
}
|
|
6400
|
+
case "resolveConflict":
|
|
6401
|
+
await api.resolveConflictAsTask(effect.sessionId, effect.taskId);
|
|
6402
|
+
await refreshPlan(deps, effect.sessionId);
|
|
6403
|
+
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." });
|
|
6404
|
+
return;
|
|
5671
6405
|
case "deleteSession":
|
|
5672
6406
|
await api.deleteSession(effect.sessionId, workspace);
|
|
5673
6407
|
await loadSessions(deps);
|
|
@@ -5700,7 +6434,7 @@ function osc52(text) {
|
|
|
5700
6434
|
function copySelection(deps, text) {
|
|
5701
6435
|
if (!text) return;
|
|
5702
6436
|
const lines = text.split("\n").length;
|
|
5703
|
-
const command = (0,
|
|
6437
|
+
const command = (0, import_core18.clipboardCopyCommand)(deps.hasBin);
|
|
5704
6438
|
if (command) {
|
|
5705
6439
|
try {
|
|
5706
6440
|
(deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
|
|
@@ -5831,7 +6565,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
5831
6565
|
case "status_update": {
|
|
5832
6566
|
const updates = {};
|
|
5833
6567
|
for (const task of event.tasks ?? []) {
|
|
5834
|
-
updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null };
|
|
6568
|
+
updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
|
|
5835
6569
|
}
|
|
5836
6570
|
dispatch({ type: "tasksStatus", updates, sessionId });
|
|
5837
6571
|
return;
|
|
@@ -5842,7 +6576,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
5842
6576
|
// The extension shows these in its checkpoint panel; here they are
|
|
5843
6577
|
// transcript lines, which is the TUI's equivalent surface.
|
|
5844
6578
|
case "checkpoint":
|
|
5845
|
-
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0,
|
|
6579
|
+
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core18.truncateCheckpointSummary)(event.summary)}` });
|
|
5846
6580
|
return;
|
|
5847
6581
|
case "approval_decided":
|
|
5848
6582
|
dispatch({ type: "notice", message: describeApprovalDecision2(event) });
|
|
@@ -5873,10 +6607,39 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
5873
6607
|
case "execution_stopped":
|
|
5874
6608
|
dispatch({ type: "executionComplete", stopped: true, sessionId });
|
|
5875
6609
|
return;
|
|
5876
|
-
//
|
|
5877
|
-
|
|
5878
|
-
|
|
6610
|
+
// Nothing started; the user chooses how to go on.
|
|
6611
|
+
case "isolation_blocked":
|
|
6612
|
+
dispatch({ type: "isolationBlocked", message: event.message, ...event.repos ? { repos: event.repos } : {}, sessionId });
|
|
6613
|
+
return;
|
|
6614
|
+
// How the run isolates. The daemon has no toast channel, so this is the only place the user hears it.
|
|
6615
|
+
case "notice":
|
|
6616
|
+
dispatch({ type: "notice", message: event.message });
|
|
6617
|
+
return;
|
|
6618
|
+
case "isolation_handoff":
|
|
6619
|
+
dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
|
|
5879
6620
|
return;
|
|
6621
|
+
// Raw runner chatter, and the planner's own turn: the status line and the
|
|
6622
|
+
// plan pane already say everything the user needs during a run. Listed
|
|
6623
|
+
// rather than defaulted, so a new SessionMessage variant fails to compile
|
|
6624
|
+
// here until someone decides what a run's watcher does with it.
|
|
6625
|
+
case "task_updated":
|
|
6626
|
+
case "task_output":
|
|
6627
|
+
case "plan_thinking":
|
|
6628
|
+
case "planner_liveness":
|
|
6629
|
+
case "research_step":
|
|
6630
|
+
case "plan_token":
|
|
6631
|
+
case "research_step_done":
|
|
6632
|
+
case "approval_request":
|
|
6633
|
+
case "approval_settled":
|
|
6634
|
+
return;
|
|
6635
|
+
// Its asker already has the result, from the merge request itself.
|
|
6636
|
+
case "isolation_merge":
|
|
6637
|
+
return;
|
|
6638
|
+
default: {
|
|
6639
|
+
const unhandled = event;
|
|
6640
|
+
void unhandled;
|
|
6641
|
+
return;
|
|
6642
|
+
}
|
|
5880
6643
|
}
|
|
5881
6644
|
}
|
|
5882
6645
|
async function refreshPlan(deps, sessionId) {
|
|
@@ -6195,7 +6958,7 @@ async function handleTui(subArgs) {
|
|
|
6195
6958
|
}
|
|
6196
6959
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
6197
6960
|
try {
|
|
6198
|
-
(0,
|
|
6961
|
+
(0, import_core19.assertWorkspaceExists)(workspace);
|
|
6199
6962
|
} catch {
|
|
6200
6963
|
console.error(
|
|
6201
6964
|
`Workspace "${workspace}" does not exist.
|
|
@@ -6204,7 +6967,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6204
6967
|
);
|
|
6205
6968
|
process.exit(1);
|
|
6206
6969
|
}
|
|
6207
|
-
registerSkillCommands((0,
|
|
6970
|
+
registerSkillCommands((0, import_core19.createSkillsService)(workspace).listSkills());
|
|
6208
6971
|
const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
|
|
6209
6972
|
const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
|
|
6210
6973
|
const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
|
|
@@ -6223,7 +6986,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6223
6986
|
conversationQueue,
|
|
6224
6987
|
port,
|
|
6225
6988
|
dispatch: (action) => app.dispatch(action),
|
|
6226
|
-
newSessionId: () => (0,
|
|
6989
|
+
newSessionId: () => (0, import_core19.mintSessionId)(),
|
|
6227
6990
|
setEnvVar: (key, value) => {
|
|
6228
6991
|
writeEnvVar(findEnvFile(), key, value);
|
|
6229
6992
|
process.env[key] = value;
|
|
@@ -6311,6 +7074,10 @@ var COMMANDS = {
|
|
|
6311
7074
|
"task-deps": handleTaskDeps,
|
|
6312
7075
|
terminal: handleTerminal,
|
|
6313
7076
|
"sessions": handleSessions,
|
|
7077
|
+
fork: handleFork,
|
|
7078
|
+
compact: handleCompact,
|
|
7079
|
+
rewind: handleRewind,
|
|
7080
|
+
handoff: handleHandoff,
|
|
6314
7081
|
tui: handleTui
|
|
6315
7082
|
};
|
|
6316
7083
|
|
|
@@ -6329,7 +7096,7 @@ function cliVersion() {
|
|
|
6329
7096
|
|
|
6330
7097
|
// src/main.ts
|
|
6331
7098
|
async function main() {
|
|
6332
|
-
(0,
|
|
7099
|
+
(0, import_core20.migrateOldConfigDir)();
|
|
6333
7100
|
loadEnvFile();
|
|
6334
7101
|
const argv = process.argv.slice(2);
|
|
6335
7102
|
const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");
|