@ordewell/cli 0.4.23 → 0.5.1

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