@ordewell/cli 0.5.3 → 0.5.5

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