@ordewell/cli 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +618 -184
  2. package/package.json +4 -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_core30 = require("@ordewell/core");
27
+ var import_core33 = require("@ordewell/core");
28
28
 
29
29
  // src/utils/env.ts
30
30
  var import_fs = require("fs");
@@ -33,42 +33,56 @@ var import_core = require("@ordewell/core");
33
33
  function findEnvFile() {
34
34
  return (0, import_path.join)((0, import_core.globalDataDir)(), ".env");
35
35
  }
36
+ function unquote(value) {
37
+ const quote = value[0];
38
+ return value.length >= 2 && (quote === '"' || quote === "'") && value.endsWith(quote) ? value.slice(1, -1) : value;
39
+ }
36
40
  function loadEnvFile() {
37
41
  const filePath = findEnvFile();
38
42
  if (!(0, import_fs.existsSync)(filePath)) return;
39
43
  const content = (0, import_fs.readFileSync)(filePath, "utf8");
40
44
  for (const line of content.split("\n")) {
41
- const trimmed = line.trim();
45
+ const trimmed = line.trim().replace(/^export\s+/, "");
42
46
  if (!trimmed || trimmed.startsWith("#")) continue;
43
47
  const eq = trimmed.indexOf("=");
44
48
  if (eq === -1) continue;
45
49
  const key = trimmed.slice(0, eq).trim();
46
- const value = trimmed.slice(eq + 1).trim();
50
+ const value = unquote(trimmed.slice(eq + 1).trim());
47
51
  if (key && process.env[key] === void 0) {
48
52
  process.env[key] = value;
49
53
  }
50
54
  }
51
55
  }
56
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
57
+ function assertValidEnvKey(key) {
58
+ if (!ENV_KEY_PATTERN.test(key)) {
59
+ throw new Error(`"${key}" is not a valid environment variable name.`);
60
+ }
61
+ }
52
62
  function writeEnvVar(filePath, key, value) {
53
- try {
54
- const { dir } = (0, import_path.parse)(filePath);
55
- if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
56
- let content = "";
57
- if ((0, import_fs.existsSync)(filePath)) {
58
- content = (0, import_fs.readFileSync)(filePath, "utf8");
59
- const regex = new RegExp(`^${key}=.*$`, "m");
60
- if (regex.test(content)) {
61
- content = content.replace(regex, `${key}=${value}`);
62
- } else {
63
- content += `
64
- ${key}=${value}
63
+ assertValidEnvKey(key);
64
+ if (/[\r\n]/.test(value)) {
65
+ throw new Error(`Refusing to write ${key}: the value contains a newline.`);
66
+ }
67
+ let content = "";
68
+ if ((0, import_fs.existsSync)(filePath)) {
69
+ content = (0, import_fs.readFileSync)(filePath, "utf8");
70
+ const lines = content.split("\n");
71
+ const index = lines.findIndex((line) => line.startsWith(`${key}=`));
72
+ if (index === -1) {
73
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
74
+ content += `${key}=${value}
65
75
  `;
66
- }
67
76
  } else {
68
- content = `${key}=${value}
69
- `;
77
+ lines[index] = `${key}=${value}`;
78
+ content = lines.join("\n");
70
79
  }
71
- (0, import_fs.writeFileSync)(filePath, content);
80
+ } else {
81
+ content = `${key}=${value}
82
+ `;
83
+ }
84
+ try {
85
+ (0, import_core.writePrivateFile)(filePath, content);
72
86
  } catch (err) {
73
87
  console.error(`Could not write to ${filePath}: ${err.message}`);
74
88
  }
@@ -106,6 +120,7 @@ Tasks (<id> is an order number or a task ID):
106
120
  ordewell force-start <id> Start a task now, ignoring dependencies
107
121
  ordewell run-task <id> Run only one task
108
122
  ordewell retry <id> Re-run a failed task
123
+ ordewell continue <id> <msg> Continue a finished task in its saved runner session
109
124
  ordewell cancel <id> Kill a running task
110
125
  ordewell terminal <id> Open a real terminal attached to a task's runner
111
126
 
@@ -124,6 +139,7 @@ Planner, models and runners:
124
139
  ordewell runners [<id> on|off] Enable or disable runners (claude-code, opencode, codex)
125
140
  ordewell allowlist set|clear|show Limit which models a runner may use
126
141
  ordewell auto [on|off] Autonomous permission mode for new sessions
142
+ ordewell transport [terminal|structured] Experimental: run Claude Code tasks over its protocol, from the next run
127
143
  ordewell parallel [<n>] How many AI tasks run at once (default 3; applies to a live run)
128
144
  ordewell refresh Re-discover runners and model catalogs
129
145
  ordewell models List every provider's catalog (works without a server)
@@ -583,6 +599,40 @@ var ApiClient = class _ApiClient {
583
599
  }
584
600
  return res.data;
585
601
  }
602
+ /** A user message to a structured task: delivered now if it waits for input, else queued behind its turn. */
603
+ async sendTaskMessage(sessionId, taskId, text) {
604
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, { text });
605
+ if (res.status !== 200) {
606
+ throw new Error(res.data?.error || "Send message failed");
607
+ }
608
+ return res.data;
609
+ }
610
+ /** Continue a finished structured task in its saved session, with `text` as its next turn. */
611
+ async continueTask(sessionId, taskId, text) {
612
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, { text });
613
+ if (res.status !== 200) {
614
+ throw new Error(res.data?.error || "Continue failed");
615
+ }
616
+ return res.data;
617
+ }
618
+ /** `removed` is false once the message was already delivered. */
619
+ async removeQueuedTaskMessage(sessionId, taskId, messageId) {
620
+ const res = await this.httpRequest(
621
+ "DELETE",
622
+ `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`
623
+ );
624
+ if (res.status !== 200) {
625
+ throw new Error(res.data?.error || "Remove message failed");
626
+ }
627
+ return res.data;
628
+ }
629
+ async interruptTask(sessionId, taskId) {
630
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`);
631
+ if (res.status !== 200) {
632
+ throw new Error(res.data?.error || "Interrupt failed");
633
+ }
634
+ return res.data;
635
+ }
586
636
  async addTask(sessionId, task) {
587
637
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks`, task);
588
638
  if (res.status !== 200) {
@@ -742,6 +792,20 @@ var ApiClient = class _ApiClient {
742
792
  }
743
793
  return res.data;
744
794
  }
795
+ /** The attempts of a structured task that have a saved log, oldest first. */
796
+ async getTaskLogAttempts(sessionId, taskId, workspace) {
797
+ const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
798
+ const res = await this.httpRequest("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`);
799
+ if (res.status !== 200) throw new Error(res.data?.error || "Could not read the task log");
800
+ return res.data.attempts;
801
+ }
802
+ /** One attempt's saved log, for `replayTaskLog`. */
803
+ async getTaskLog(sessionId, taskId, attempt, workspace) {
804
+ const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
805
+ const res = await this.httpRequest("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`);
806
+ if (res.status !== 200) throw new Error(res.data?.error || "Could not read the task log");
807
+ return res.data.events;
808
+ }
745
809
  /**
746
810
  * The one place a session socket is built, so the token travels on both
747
811
  * stream paths. It rides as a subprotocol rather than a query parameter, so
@@ -808,12 +872,16 @@ var ApiClient = class _ApiClient {
808
872
  });
809
873
  });
810
874
  }
811
- /** Answer a planner approval prompt. Requests arrive over the session socket. */
812
- async respondToApproval(sessionId, approvalId, granted) {
875
+ /**
876
+ * Answer an approval prompt: the planner's with a yes/no, a task runner's
877
+ * with the whole decision (ADR-0018, A1). The planner's requests arrive over
878
+ * the session socket; a runner's, in its task log.
879
+ */
880
+ async respondToApproval(sessionId, approvalId, answer) {
813
881
  const { status, data } = await this.httpRequest(
814
882
  "POST",
815
883
  `/api/approvals/${encodeURIComponent(sessionId)}/${encodeURIComponent(approvalId)}`,
816
- { granted }
884
+ typeof answer === "boolean" ? { granted: answer } : answer
817
885
  );
818
886
  if (status !== 200) throw new Error(`Failed to answer approval ${approvalId} (HTTP ${status})`);
819
887
  return data;
@@ -1931,6 +1999,7 @@ ${providerLabel(provider)}`);
1931
1999
 
1932
2000
  // src/commands/setup.ts
1933
2001
  var import_fs6 = require("fs");
2002
+ var import_core13 = require("@ordewell/core");
1934
2003
  function handleSetup(subArgs) {
1935
2004
  const modelIdx = subArgs.indexOf("--model");
1936
2005
  const chosenModel = modelIdx !== -1 ? subArgs[modelIdx + 1] : null;
@@ -2002,15 +2071,15 @@ function handleSetup(subArgs) {
2002
2071
  # STEPFUN_API_KEY=...
2003
2072
  # SILICONFLOW_API_KEY=...
2004
2073
  `;
2005
- (0, import_fs6.writeFileSync)(envFile, content);
2074
+ (0, import_core13.writePrivateFile)(envFile, content);
2006
2075
  console.log(`Wrote: ${envFile}`);
2007
2076
  console.log("Review and edit the file, then run: ordewell tui");
2008
2077
  }
2009
2078
 
2010
2079
  // src/commands/plugins.ts
2011
- var import_core13 = require("@ordewell/core");
2080
+ var import_core14 = require("@ordewell/core");
2012
2081
  function handlePlugins(subArgs) {
2013
- const registry = new import_core13.RunnerRegistry();
2082
+ const registry = new import_core14.RunnerRegistry();
2014
2083
  registry.loadUserPlugins();
2015
2084
  const sub = subArgs[0];
2016
2085
  if (sub === "list" || !sub) {
@@ -2046,7 +2115,7 @@ ${plugins.length} plugin(s):
2046
2115
  process.exit(1);
2047
2116
  }
2048
2117
  try {
2049
- const resolved = (0, import_core13.classifyPluginSource)(source);
2118
+ const resolved = (0, import_core14.classifyPluginSource)(source);
2050
2119
  const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
2051
2120
  console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
2052
2121
  } catch (err) {
@@ -2207,6 +2276,22 @@ async function handleVerify(subArgs, api) {
2207
2276
  }
2208
2277
  }
2209
2278
 
2279
+ // src/commands/transport.ts
2280
+ var describe = (transport) => `Runner transport (experimental): ${transport === "structured" ? "structured" : "terminal"} \u2014 applies from the next run`;
2281
+ async function handleTransport(subArgs, api) {
2282
+ const action = subArgs[0];
2283
+ const port = api ? void 0 : await ensureDaemon(resolvePort(subArgs));
2284
+ const client = api || new ApiClient(port);
2285
+ if (action === "terminal" || action === "structured") {
2286
+ const result = await client.sendCommand("transport", { action });
2287
+ console.log(describe(result.settings?.runnerTransport));
2288
+ } else {
2289
+ const settings = await client.getSettings();
2290
+ console.log(describe(settings.runnerTransport));
2291
+ console.log("Usage: ordewell transport [terminal|structured]");
2292
+ }
2293
+ }
2294
+
2210
2295
  // src/commands/task-control.ts
2211
2296
  var PAST_TENSE = {
2212
2297
  run: "Task started.",
@@ -2263,6 +2348,23 @@ function makeHandler(action, command = action) {
2263
2348
  );
2264
2349
  };
2265
2350
  }
2351
+ async function handleContinue(subArgs, injectedApi) {
2352
+ const usage = "Usage: ordewell continue <task-id-or-order> <message> [--session-id <id>] [--workspace /path]";
2353
+ const message = positionals(subArgs).slice(1).join(" ").trim();
2354
+ if (!message) {
2355
+ console.error(usage);
2356
+ process.exit(1);
2357
+ }
2358
+ await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
2359
+ try {
2360
+ await api.continueTask(sessionId, taskId, message);
2361
+ console.log("Task continued.");
2362
+ } catch (err) {
2363
+ console.error(`Failed to continue task: ${err.message}`);
2364
+ process.exit(1);
2365
+ }
2366
+ });
2367
+ }
2266
2368
  var handleRunTask = makeHandler("run", "run-task");
2267
2369
  var handleForceStart = makeHandler("force-start");
2268
2370
  var handleRetry = makeHandler("retry");
@@ -2515,7 +2617,7 @@ async function handleRewind(subArgs, injectedApi) {
2515
2617
  var import_readline2 = require("readline");
2516
2618
 
2517
2619
  // src/tui/transcript.ts
2518
- var import_core14 = require("@ordewell/core");
2620
+ var import_core15 = require("@ordewell/core");
2519
2621
  function show(state, { view: conversation, gate: turnGate }) {
2520
2622
  const gated = turnGate === state.turnGate ? state : { ...state, turnGate };
2521
2623
  if (conversation === state.conversation) return gated;
@@ -2523,16 +2625,16 @@ function show(state, { view: conversation, gate: turnGate }) {
2523
2625
  return { ...gated, conversation, scroll };
2524
2626
  }
2525
2627
  function fold(state, input) {
2526
- return show(state, { view: (0, import_core14.reduceConversation)(state.conversation, input), gate: state.turnGate });
2628
+ return show(state, { view: (0, import_core15.reduceConversation)(state.conversation, input), gate: state.turnGate });
2527
2629
  }
2528
2630
  function say(state, role, text) {
2529
2631
  return fold(state, { type: "local_entry", role, text });
2530
2632
  }
2531
2633
  function hear(state, message) {
2532
- return show(state, (0, import_core14.followTurn)(state.conversation, state.turnGate, message));
2634
+ return show(state, (0, import_core15.followTurn)(state.conversation, state.turnGate, message));
2533
2635
  }
2534
2636
  function cutTurn(state) {
2535
- return show(state, (0, import_core14.stopTurn)(state.conversation, state.turnGate));
2637
+ return show(state, (0, import_core15.stopTurn)(state.conversation, state.turnGate));
2536
2638
  }
2537
2639
  function wiped(view) {
2538
2640
  return { ...view, blocks: view.blocks.filter((b) => b.type === "usage") };
@@ -2677,7 +2779,13 @@ var style = {
2677
2779
  blue: colour("34"),
2678
2780
  magenta: colour("35"),
2679
2781
  cyan: colour("36"),
2680
- grey: colour("90")
2782
+ grey: colour("90"),
2783
+ /**
2784
+ * The task view's accent: a bright blue, separate from the planner chat's
2785
+ * cyan and magenta. Bright rather than the plain 34, which reads as black on
2786
+ * a dark background.
2787
+ */
2788
+ accent: colour("94")
2681
2789
  };
2682
2790
 
2683
2791
  // src/tui/handoff.ts
@@ -2927,7 +3035,7 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
2927
3035
  }
2928
3036
 
2929
3037
  // src/commands/planner.ts
2930
- var import_core15 = require("@ordewell/core");
3038
+ var import_core16 = require("@ordewell/core");
2931
3039
 
2932
3040
  // src/plannerModelSwitch.ts
2933
3041
  function describePlannerSwitch(settings, provider) {
@@ -2946,7 +3054,7 @@ function rememberedModelFor(settings, provider) {
2946
3054
  }
2947
3055
 
2948
3056
  // src/commands/planner.ts
2949
- var KNOWN_PROVIDERS = Object.keys(import_core15.ALL_PROVIDERS);
3057
+ var KNOWN_PROVIDERS = Object.keys(import_core16.ALL_PROVIDERS);
2950
3058
  async function handlePlanner(subArgs, injectedApi) {
2951
3059
  const api = await connect(subArgs, injectedApi);
2952
3060
  const [wanted] = positionals(subArgs);
@@ -2961,13 +3069,13 @@ async function handlePlanner(subArgs, injectedApi) {
2961
3069
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
2962
3070
  const recall2 = describePlannerSwitch(settings, provider);
2963
3071
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
2964
- const meta = import_core15.ALL_PROVIDERS[provider];
3072
+ const meta = import_core16.ALL_PROVIDERS[provider];
2965
3073
  const catalog = await fetchCatalog(api);
2966
3074
  console.log(
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}.`
3075
+ (0, import_core16.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2968
3076
  );
2969
3077
  console.log(plannerModelLine(recall2));
2970
- if (!(0, import_core15.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3078
+ if (!(0, import_core16.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2971
3079
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
2972
3080
  }
2973
3081
  }
@@ -2985,15 +3093,15 @@ async function showPlanners(api) {
2985
3093
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
2986
3094
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2987
3095
  console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
2988
- for (const id of import_core15.CLI_PROVIDERS) {
2989
- const runner = (0, import_core15.runnerForProvider)(id);
3096
+ for (const id of import_core16.CLI_PROVIDERS) {
3097
+ const runner = (0, import_core16.runnerForProvider)(id);
2990
3098
  const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
2991
- console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core15.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
3099
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core16.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
2992
3100
  }
2993
3101
  console.log("\nAPI providers");
2994
- for (const id of import_core15.PROVIDER_PRIORITY) {
3102
+ for (const id of import_core16.PROVIDER_PRIORITY) {
2995
3103
  const configured = catalog.providers.includes(id);
2996
- const meta = import_core15.ALL_PROVIDERS[id];
3104
+ const meta = import_core16.ALL_PROVIDERS[id];
2997
3105
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
2998
3106
  }
2999
3107
  console.log(`
@@ -3010,7 +3118,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3010
3118
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3011
3119
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
3012
3120
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
3013
- const runner = (0, import_core15.runnerForProvider)(provider);
3121
+ const runner = (0, import_core16.runnerForProvider)(provider);
3014
3122
  if (!runner) {
3015
3123
  fail(
3016
3124
  "Thinking effort applies to a coding-agent planner.",
@@ -3050,7 +3158,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3050
3158
  }
3051
3159
 
3052
3160
  // src/commands/parallel.ts
3053
- var import_core16 = require("@ordewell/core");
3161
+ var import_core17 = require("@ordewell/core");
3054
3162
  var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
3055
3163
  async function handleParallel(subArgs, injectedApi) {
3056
3164
  const [wanted] = positionals(subArgs);
@@ -3060,14 +3168,14 @@ async function handleParallel(subArgs, injectedApi) {
3060
3168
  console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
3061
3169
  return;
3062
3170
  }
3063
- const limit = (0, import_core16.parseMaxParallel)(wanted);
3171
+ const limit = (0, import_core17.parseMaxParallel)(wanted);
3064
3172
  if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
3065
3173
  await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
3066
3174
  console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
3067
3175
  }
3068
3176
 
3069
3177
  // src/commands/key.ts
3070
- var import_core17 = require("@ordewell/core");
3178
+ var import_core18 = require("@ordewell/core");
3071
3179
  var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
3072
3180
  async function handleKey(subArgs, injectedApi) {
3073
3181
  const [action, provider, ...keyParts] = positionals(subArgs);
@@ -3078,8 +3186,8 @@ async function handleKey(subArgs, injectedApi) {
3078
3186
  if (action !== "set") {
3079
3187
  const catalog = await fetchCatalog(api);
3080
3188
  console.log("\nAPI provider keys\n");
3081
- for (const id2 of import_core17.PROVIDER_PRIORITY) {
3082
- const meta2 = import_core17.ALL_PROVIDERS[id2];
3189
+ for (const id2 of import_core18.PROVIDER_PRIORITY) {
3190
+ const meta2 = import_core18.ALL_PROVIDERS[id2];
3083
3191
  const configured = catalog.providers.includes(id2);
3084
3192
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
3085
3193
  }
@@ -3089,7 +3197,7 @@ async function handleKey(subArgs, injectedApi) {
3089
3197
  }
3090
3198
  if (!provider) fail(USAGE6);
3091
3199
  const id = provider.toLowerCase();
3092
- const meta = import_core17.ALL_PROVIDERS[id];
3200
+ const meta = import_core18.ALL_PROVIDERS[id];
3093
3201
  if (!meta) {
3094
3202
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
3095
3203
  }
@@ -3170,7 +3278,7 @@ async function handleApprove(subArgs, injectedApi) {
3170
3278
  // src/tui/terminalLauncher.ts
3171
3279
  var import_child_process3 = require("child_process");
3172
3280
  var import_util = require("util");
3173
- var import_core18 = require("@ordewell/core");
3281
+ var import_core19 = require("@ordewell/core");
3174
3282
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3175
3283
  var defaultExecFile = async (command, args) => {
3176
3284
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3190,16 +3298,16 @@ var defaultWhich = (bin) => {
3190
3298
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3191
3299
  const platform = deps.platform ?? process.platform;
3192
3300
  const env = deps.env ?? process.env;
3193
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core18.hasTmux;
3301
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core19.hasTmux;
3194
3302
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3195
3303
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3196
3304
  const which = deps.which ?? defaultWhich;
3197
3305
  if (!hasTmuxImpl()) {
3198
3306
  return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
3199
3307
  }
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);
3308
+ const session = (0, import_core19.tmuxSessionName)(port);
3309
+ const socket = (0, import_core19.tmuxSocketName)(port);
3310
+ const base = (0, import_core19.tmuxWindowName)(taskId, planSessionId);
3203
3311
  let windows;
3204
3312
  try {
3205
3313
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3493,7 +3601,7 @@ Dependencies \xB7 #${task.order} ${task.title}
3493
3601
  }
3494
3602
 
3495
3603
  // src/tui/index.ts
3496
- var import_core29 = require("@ordewell/core");
3604
+ var import_core32 = require("@ordewell/core");
3497
3605
 
3498
3606
  // src/tui/editor.ts
3499
3607
  function emptyEditor() {
@@ -3641,7 +3749,8 @@ var SLASH_COMMANDS = [
3641
3749
  { name: "retry", usage: "/retry <id>", description: "Re-run a failed task", category: "tasks" },
3642
3750
  { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
3643
3751
  { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
3644
- { name: "terminal", usage: "/terminal <id>", description: "Open a real terminal attached to a task's runner", category: "tasks" },
3752
+ { name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
3753
+ { name: "terminal", usage: "/terminal <id>", description: "Open a task: its OS terminal, or a structured task's live log", category: "tasks" },
3645
3754
  { name: "task-runner", usage: "/task-runner <id> [runner]", description: "Choose or set a task's executor (re-picks its model and mode)", category: "tasks" },
3646
3755
  { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
3647
3756
  { name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
@@ -3655,6 +3764,7 @@ var SLASH_COMMANDS = [
3655
3764
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3656
3765
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3657
3766
  { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3767
+ { name: "transport", usage: "/transport [terminal|structured]", description: "Experimental: run Claude Code tasks over its protocol instead of a terminal, from the next run", category: "models" },
3658
3768
  { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3659
3769
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3660
3770
  // Skills
@@ -3739,7 +3849,7 @@ function tokenCompletions(token) {
3739
3849
  }
3740
3850
 
3741
3851
  // src/tui/blocks.ts
3742
- var import_core19 = require("@ordewell/core");
3852
+ var import_core20 = require("@ordewell/core");
3743
3853
 
3744
3854
  // src/tui/markdown.ts
3745
3855
  function renderMarkdown(source, cols) {
@@ -3872,13 +3982,14 @@ function tailOf(text, max) {
3872
3982
  var ROLE_PREFIX = {
3873
3983
  user: (t) => `${style.cyan("\u276F")} ${t}`,
3874
3984
  planner: (t) => `${style.magenta("\u25C6")} ${t}`,
3985
+ agent: (t) => `${style.magenta("\u25C6")} ${t}`,
3875
3986
  system: (t) => style.grey(`\xB7 ${t}`),
3876
3987
  error: (t) => `${style.red("\u2717")} ${style.red(t)}`
3877
3988
  };
3878
3989
  function messageLines(block, cols) {
3879
3990
  const text = sanitize(block.text);
3880
3991
  const room = Math.max(1, cols - 2);
3881
- const wrapped = block.role === "planner" ? renderMarkdown(text, room) : wrap(text, room);
3992
+ const wrapped = block.role === "planner" || block.role === "agent" ? renderMarkdown(text, room) : wrap(text, room);
3882
3993
  return wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[block.role](line) : ` ${line}`);
3883
3994
  }
3884
3995
  function thinkingLines(block, cols, detailAll) {
@@ -3946,16 +4057,16 @@ function resultLines(block, room, detailAll) {
3946
4057
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
3947
4058
  const outcome = unsettledOutcome(block);
3948
4059
  if (outcome) {
3949
- const [reason, ...rest] = (0, import_core19.outputLines)(block.output).map(sanitize);
4060
+ const [reason, ...rest] = (0, import_core20.outputLines)(block.output).map(sanitize);
3950
4061
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
3951
4062
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
3952
4063
  }
3953
4064
  if (detailAll) {
3954
- const lines2 = (0, import_core19.outputLines)(block.output).map(sanitize);
4065
+ const lines2 = (0, import_core20.outputLines)(block.output).map(sanitize);
3955
4066
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
3956
4067
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
3957
4068
  }
3958
- const { lines, hiddenLineCount } = (0, import_core19.outputPreview)(block.output, PREVIEW_LINES);
4069
+ const { lines, hiddenLineCount } = (0, import_core20.outputPreview)(block.output, PREVIEW_LINES);
3959
4070
  if (lines.length === 0) return [style.grey("(no output)")];
3960
4071
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
3961
4072
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
@@ -3996,7 +4107,8 @@ function subagentLines(block, cols, detailAll) {
3996
4107
  var APPROVAL_KIND = {
3997
4108
  shell_command: "Run a command",
3998
4109
  url_fetch: "Fetch a URL",
3999
- external_path: "Read outside the workspace"
4110
+ external_path: "Read outside the workspace",
4111
+ runner_tool: "Use a tool"
4000
4112
  };
4001
4113
  var DECIDED_BY = {
4002
4114
  "pre-approved": "pre-approved",
@@ -4006,8 +4118,9 @@ var DECIDED_BY = {
4006
4118
  };
4007
4119
  function approvalLine(block, cols) {
4008
4120
  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)}`;
4121
+ const [mark, status, paint] = block.status === "pending" ? ["?", "Waiting for you", style.yellow] : block.status === "granted" ? ["\u2713", policy ? `Auto-approved (${policy})` : block.forTask ? "Approved for this task" : "Approved", style.green] : block.status === "withdrawn" ? ["\xB7", "Withdrawn", style.grey] : ["\u2298", policy ? `Auto-denied (${policy})` : "Denied", style.yellow];
4122
+ const note = block.note ? ` \u2014 \u201C${firstLine(block.note)}\u201D` : "";
4123
+ const line = `${mark} ${status} \xB7 ${APPROVAL_KIND[block.kind]}: ${firstLine(block.subject)}${note}`;
4011
4124
  return `${paint(mark)}${truncate(line, cols).slice(mark.length)}`;
4012
4125
  }
4013
4126
  function planLine(block, cols) {
@@ -4073,38 +4186,8 @@ function tokenLine(blocks, cols) {
4073
4186
  return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
4074
4187
  }
4075
4188
 
4076
- // src/tui/geometry.ts
4077
- var PLAN_MIN_COLS = 32;
4078
- var PLAN_MAX_COLS = 64;
4079
- var PANE_INDENT = 4;
4080
- var CHAT_PROMPT_COLS = 2;
4081
- function planPaneWidth(state) {
4082
- if (state.tasks.length === 0) return 0;
4083
- const wanted = Math.min(PLAN_MAX_COLS, Math.floor(state.cols * 0.46));
4084
- return wanted >= PLAN_MIN_COLS && state.cols - wanted > PLAN_MIN_COLS ? wanted : 0;
4085
- }
4086
- function chatPaneWidth(state) {
4087
- const plan = planPaneWidth(state);
4088
- return plan === 0 ? state.cols : state.cols - plan - 1;
4089
- }
4090
- function paneColumns(state, pane) {
4091
- const plan = planPaneWidth(state);
4092
- if (plan === 0) return { first: 1, last: state.cols };
4093
- const chat = chatPaneWidth(state);
4094
- return pane === "chat" ? { first: 1, last: chat } : { first: chat + 2, last: state.cols };
4095
- }
4096
- function paneTextRoom(paneCols) {
4097
- return Math.max(1, paneCols - PANE_INDENT);
4098
- }
4099
- function taskEditorRoom(state) {
4100
- return paneTextRoom(planPaneWidth(state));
4101
- }
4102
- function chatEditorRoom(cols, active) {
4103
- return Math.max(1, cols - CHAT_PROMPT_COLS - (active ? 1 : 0));
4104
- }
4105
-
4106
4189
  // src/tui/state.ts
4107
- var import_core20 = require("@ordewell/core");
4190
+ var import_core21 = require("@ordewell/core");
4108
4191
  var SKILL_IDS = ["tdd", "verify"];
4109
4192
  function noSkills() {
4110
4193
  return { tdd: false, verify: false };
@@ -4134,6 +4217,12 @@ function planRows(state) {
4134
4217
  function selectedPlanRow(state) {
4135
4218
  return planRows(state)[state.selectedTask] ?? null;
4136
4219
  }
4220
+ function waitingApproval(tv) {
4221
+ return tv.view.blocks.find((b) => b.type === "approval" && b.status === "pending" && b.approvalId !== void 0);
4222
+ }
4223
+ function continuesTask(task) {
4224
+ return task?.continuable === true && (task.status === "completed" || task.status === "failed");
4225
+ }
4137
4226
  function findTask(tasks, id) {
4138
4227
  for (const task of tasks) {
4139
4228
  if (task.id === id) return task;
@@ -4145,8 +4234,9 @@ function findTask(tasks, id) {
4145
4234
  function initialState(overrides = {}) {
4146
4235
  return {
4147
4236
  editor: emptyEditor(),
4148
- conversation: import_core20.EMPTY_CONVERSATION,
4149
- turnGate: import_core20.NO_TURN,
4237
+ conversation: import_core21.EMPTY_CONVERSATION,
4238
+ taskView: null,
4239
+ turnGate: import_core21.NO_TURN,
4150
4240
  detailAll: false,
4151
4241
  status: "idle",
4152
4242
  busyLabel: "",
@@ -4171,17 +4261,18 @@ function initialState(overrides = {}) {
4171
4261
  orchestratorModel: "",
4172
4262
  plannerProvider: "",
4173
4263
  plannerEffort: "",
4174
- maxParallel: import_core20.DEFAULT_MAX_PARALLEL,
4264
+ maxParallel: import_core21.DEFAULT_MAX_PARALLEL,
4175
4265
  configuredProviders: [],
4176
4266
  allowlist: {},
4177
4267
  autonomous: true,
4268
+ runnerTransport: "terminal",
4178
4269
  mouseCapture: true,
4179
4270
  selection: null,
4180
4271
  workspace: process.cwd(),
4181
4272
  handoff: null,
4182
4273
  overlay: null,
4183
4274
  pendingApprovals: [],
4184
- queuedPrompts: import_core20.EMPTY_HOLD,
4275
+ queuedPrompts: import_core21.EMPTY_HOLD,
4185
4276
  stopArmed: false,
4186
4277
  stopArmToken: 0,
4187
4278
  stopRequested: false,
@@ -4194,19 +4285,53 @@ function initialState(overrides = {}) {
4194
4285
  };
4195
4286
  }
4196
4287
 
4288
+ // src/tui/geometry.ts
4289
+ var PLAN_MIN_COLS = 32;
4290
+ var PLAN_MAX_COLS = 64;
4291
+ var PANE_INDENT = 4;
4292
+ function planPaneWidth(state) {
4293
+ if (state.tasks.length === 0) return 0;
4294
+ const wanted = Math.min(PLAN_MAX_COLS, Math.floor(state.cols * 0.46));
4295
+ return wanted >= PLAN_MIN_COLS && state.cols - wanted > PLAN_MIN_COLS ? wanted : 0;
4296
+ }
4297
+ function chatPaneWidth(state) {
4298
+ const plan = planPaneWidth(state);
4299
+ return plan === 0 ? state.cols : state.cols - plan - 1;
4300
+ }
4301
+ function paneColumns(state, pane) {
4302
+ const plan = planPaneWidth(state);
4303
+ if (plan === 0) return { first: 1, last: state.cols };
4304
+ const chat = chatPaneWidth(state);
4305
+ return pane === "chat" ? { first: 1, last: chat } : { first: chat + 2, last: state.cols };
4306
+ }
4307
+ function paneTextRoom(paneCols) {
4308
+ return Math.max(1, paneCols - PANE_INDENT);
4309
+ }
4310
+ function taskEditorRoom(state) {
4311
+ return paneTextRoom(planPaneWidth(state));
4312
+ }
4313
+ function chatPromptLabel(state) {
4314
+ if (!state.taskView) return "\u276F";
4315
+ const task = findTask(state.tasks, state.taskView.taskId);
4316
+ return `\u2192 ${continuesTask(task) ? "Continue task" : "Task"} ${task?.order ?? "?"}`;
4317
+ }
4318
+ function chatEditorRoomFor(state, active) {
4319
+ return Math.max(1, state.cols - width(chatPromptLabel(state)) - 1 - (active ? 1 : 0));
4320
+ }
4321
+
4197
4322
  // src/tui/layout.ts
4198
- var import_core21 = require("@ordewell/core");
4323
+ var import_core22 = require("@ordewell/core");
4199
4324
  var CHROME_ROWS = 4;
4200
4325
  function chatInputWrap(state) {
4201
4326
  const active = state.focus === "chat" && !state.overlay;
4202
4327
  if (!active) return null;
4203
- return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
4328
+ return wrapLines(state.editor.text, chatEditorRoomFor(state, true));
4204
4329
  }
4205
4330
  function footerHints(state) {
4206
4331
  const selected = selectedPlanRow(state)?.task;
4207
4332
  const markHint = selected?.status === "completed" ? "m undone" : "m done";
4208
4333
  const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
4209
- const planning = plannerInFlight(state);
4334
+ const planning = plannerInFlight(state) && !state.taskView;
4210
4335
  const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
4211
4336
  if (state.focus === "plan") {
4212
4337
  if (state.taskEditor) {
@@ -4234,7 +4359,7 @@ function footerHints(state) {
4234
4359
  "tab chat"
4235
4360
  ];
4236
4361
  }
4237
- const detailHint = (0, import_core21.hasHiddenDetail)(state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4362
+ const detailHint = (0, import_core22.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4238
4363
  return [
4239
4364
  "/help",
4240
4365
  "tab plan",
@@ -4252,6 +4377,43 @@ function queuedBubble(text, cols) {
4252
4377
  const room = Math.max(1, cols - width("\u25C7 \xB7 queued \xB7 esc to unsend"));
4253
4378
  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
4379
  }
4380
+ function taskQueuedRows(state, cols) {
4381
+ const tv = state.taskView;
4382
+ if (!tv) return [];
4383
+ return tv.view.queued.flatMap((message, i) => queuedTaskBubble(message.text, i === tv.queuedIndex, cols));
4384
+ }
4385
+ function queuedTaskBubble(text, selected, cols) {
4386
+ const marker2 = selected ? style.accent("\u276F") : style.grey("\u25C7");
4387
+ const tag = selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4388
+ const room = Math.max(1, cols - width(`\u276F \u25C7 ${tag}`));
4389
+ return wrap(text, room).map((line, i) => i === 0 ? truncate(`${marker2} ${selected ? style.bold(line) : line}${style.grey(tag)}`, cols) : truncate(` ${line}`, cols));
4390
+ }
4391
+ function taskActivity(task, working) {
4392
+ if (task?.awaitingApproval) return approvalLabel(task.awaitingApproval);
4393
+ if (working) return "working";
4394
+ if (task?.status === "awaiting_user") {
4395
+ return task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "waiting for your input";
4396
+ }
4397
+ return task?.status ?? "";
4398
+ }
4399
+ function taskHeaderLines(state, tv, cols) {
4400
+ const task = findTask(state.tasks, tv.taskId);
4401
+ const parts = [
4402
+ `\u2192 Task ${task?.order ?? "?"}`,
4403
+ task?.title ?? "(task removed)",
4404
+ task?.assignedRunner ?? "",
4405
+ taskActivity(task, tv.view.working)
4406
+ ].filter(Boolean);
4407
+ const approval = waitingApproval(tv);
4408
+ const hint = approval ? ["ctrl-y allow", ...approval.allowForTask ? ["ctrl-t allow for task"] : [], "ctrl-g deny (composer text is the note)"] : ["ctrl-r remove queued", "ctrl-x interrupt"];
4409
+ const index = tv.attempts.indexOf(tv.attempt);
4410
+ if (tv.attempts.length > 1) hint.push(`alt\u2190/\u2192 attempt ${index >= 0 ? index + 1 : 1}/${tv.attempts.length}`);
4411
+ hint.push("esc back");
4412
+ return [
4413
+ style.accent(truncate(parts.join(" \xB7 "), cols)),
4414
+ style.grey(truncate(hint.join(" \xB7 "), cols))
4415
+ ];
4416
+ }
4255
4417
  function stopHint(state) {
4256
4418
  return plannerInFlight(state) && state.stopArmed ? "Press Esc again to stop" : "";
4257
4419
  }
@@ -4284,24 +4446,27 @@ function chatBodyLines(blocks, cols, detailAll) {
4284
4446
  return lines;
4285
4447
  }
4286
4448
  function chatLayout(state, rows, cols) {
4287
- const welcome = state.tasks.length === 0;
4288
- const body = chatBodyLines(state.conversation.blocks, cols, state.detailAll);
4449
+ const tv = state.taskView;
4450
+ const blocks = tv ? tv.view.blocks : state.conversation.blocks;
4451
+ const welcome = !tv && state.tasks.length === 0;
4452
+ const body = chatBodyLines(blocks, cols, state.detailAll);
4289
4453
  const transcript = !welcome ? body : body.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
4290
- const bubbles = queuedPromptRows(state, cols);
4454
+ const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
4291
4455
  const lines = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
4292
- const usage = rows > 1 ? tokenLine(state.conversation.blocks, cols) : null;
4456
+ const header = tv ? taskHeaderLines(state, tv, cols) : [];
4457
+ const usage = rows > 1 ? tokenLine(blocks, cols) : null;
4293
4458
  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) };
4459
+ const room = Math.max(0, rows - header.length - footer.length);
4460
+ return { lines, header, footer, anchor: lines.length > room ? "bottom" : "top", maxScroll: Math.max(0, lines.length - room) };
4296
4461
  }
4297
4462
  function chatScrollMax(state) {
4298
4463
  return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
4299
4464
  }
4300
4465
  function plannerSetup(state) {
4301
4466
  const provider = state.plannerProvider;
4302
- const registration = import_core21.ALL_PROVIDERS[provider];
4467
+ const registration = import_core22.ALL_PROVIDERS[provider];
4303
4468
  if (!registration) return { label: "not set", ready: false };
4304
- const runner = (0, import_core21.runnerForProvider)(provider);
4469
+ const runner = (0, import_core22.runnerForProvider)(provider);
4305
4470
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4306
4471
  return { label: registration.label, ready };
4307
4472
  }
@@ -4408,6 +4573,14 @@ var STATUS_ICON = {
4408
4573
  approved: "\xB7",
4409
4574
  pending: "\xB7"
4410
4575
  };
4576
+ var AWAITING_LABEL = {
4577
+ input: "waiting for your input",
4578
+ checkpoint: "checkpoint",
4579
+ conflict: "merge conflict"
4580
+ };
4581
+ function approvalLabel(count) {
4582
+ return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
4583
+ }
4411
4584
  var IDLE_ICON = "~";
4412
4585
  var FIRST_ROW_LINE = 2;
4413
4586
  function planLayout(state, rows, cols) {
@@ -4484,7 +4657,7 @@ function taskLines(state, row, index, cols) {
4484
4657
  const icon = paint(rawIcon);
4485
4658
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
4486
4659
  const caret = selected ? style.cyan("\u276F") : " ";
4487
- const orderLabel = parent ? (0, import_core21.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4660
+ const orderLabel = parent ? (0, import_core22.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4488
4661
  const indent = parent ? " " : "";
4489
4662
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4490
4663
  const titleRoom = Math.max(1, cols - width(head));
@@ -4505,22 +4678,25 @@ function taskLines(state, row, index, cols) {
4505
4678
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4506
4679
  const runner = task.assignedRunner ?? "";
4507
4680
  const bodyPad = parent ? " " : " ";
4508
- const activity = idle ? "quiet \u2014 t opens its terminal" : running ? "working" : "";
4509
- const meta = [activity, runner, model].filter(Boolean).join(" \xB7 ");
4681
+ const structured = task.transport?.kind === "structured";
4682
+ const waiting = task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "";
4683
+ const activity = task.awaitingApproval ? `${approvalLabel(task.awaitingApproval)} \u2014 t opens it` : idle ? structured ? "quiet" : "quiet \u2014 t opens its terminal" : running ? "working" : waiting;
4684
+ const meta = [activity, runner, structured ? "structured" : "", model].filter(Boolean).join(" \xB7 ");
4510
4685
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4686
+ if (task.transport?.fallback) lines.push(style.yellow(truncate(`${bodyPad}terminal: ${task.transport.fallback}`, cols)));
4511
4687
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4512
4688
  if (task.isolation?.state === "conflict") {
4513
4689
  const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
4514
- const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core21.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4690
+ const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core22.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4515
4691
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4516
4692
  }
4517
4693
  if (task.isolation?.state === "repairing") {
4518
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core21.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4694
+ const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core22.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4519
4695
  const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
4520
4696
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4521
4697
  }
4522
4698
  if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
4523
- lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core21.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4699
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core22.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4524
4700
  }
4525
4701
  let editorLine;
4526
4702
  if (expanded) {
@@ -4596,6 +4772,9 @@ function helpLayout(rows, cols) {
4596
4772
  body.push(
4597
4773
  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")
4598
4774
  );
4775
+ body.push(
4776
+ style.grey("in a task view (t or /terminal on a structured task): ctrl-r removes the selected queued message \xB7 ctrl-x interrupts \xB7 ctrl-y allows a tool request \xB7 ctrl-t allows it for the task \xB7 ctrl-g denies it, with the composer text as the note \xB7 alt\u2190/\u2192 changes attempt \xB7 esc returns")
4777
+ );
4599
4778
  const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
4600
4779
  const room = Math.max(1, rows - 2);
4601
4780
  return { lines, room, maxScroll: Math.max(0, lines.length - room) };
@@ -4605,7 +4784,7 @@ function helpScrollMax(state) {
4605
4784
  }
4606
4785
 
4607
4786
  // src/tui/render.ts
4608
- var import_core22 = require("@ordewell/core");
4787
+ var import_core23 = require("@ordewell/core");
4609
4788
  function render(state) {
4610
4789
  const { rows, cols } = state;
4611
4790
  const rowsForBody = bodyRows(state);
@@ -4614,7 +4793,7 @@ function render(state) {
4614
4793
  renderSkills(state, cols),
4615
4794
  ...body,
4616
4795
  renderStatus(state, cols),
4617
- ...renderInput(state, cols),
4796
+ ...renderInput(state),
4618
4797
  ...renderFooter(state, cols)
4619
4798
  ];
4620
4799
  const frame2 = lines.slice(Math.max(0, lines.length - rows)).map((l) => pad(l, cols));
@@ -4701,7 +4880,8 @@ function renderSkills(state, cols) {
4701
4880
  (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
4702
4881
  );
4703
4882
  const auto = state.autonomous ? style.yellow("\u25CF auto") : style.grey("\u25CB auto");
4704
- return truncate([...badges, auto].join(" "), cols);
4883
+ const structured = state.runnerTransport === "structured" ? [style.yellow("\u25CF structured")] : [];
4884
+ return truncate([...badges, auto, ...structured].join(" "), cols);
4705
4885
  }
4706
4886
  function scrolledBackMark(state) {
4707
4887
  return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
@@ -4746,10 +4926,10 @@ function paintSpans(text, base, spans) {
4746
4926
  }
4747
4927
  return out + text.slice(cursor);
4748
4928
  }
4749
- function renderInput(state, cols) {
4750
- const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
4929
+ function renderInput(state) {
4930
+ const prompt = state.taskView ? style.accent(`${chatPromptLabel(state)} `) : state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
4751
4931
  const lines = chatInputWrap(state);
4752
- const room = chatEditorRoom(cols, lines !== null);
4932
+ const room = chatEditorRoomFor(state, lines !== null);
4753
4933
  const { text, cursor } = state.editor;
4754
4934
  const continuation = " ".repeat(width(prompt));
4755
4935
  const spans = highlightSpans(text);
@@ -4792,16 +4972,18 @@ function renderBody(state, rows, cols) {
4792
4972
  const chatCols = chatPaneWidth(state);
4793
4973
  const chat = renderChat(state, rows, chatCols);
4794
4974
  const plan = renderPlan(state, rows, planCols);
4795
- const divider = `${ERASE_TO_END}${atColumn(chatCols + 1)}${style.grey("\u2502")}`;
4975
+ const border = state.taskView ? style.accent("\u2502") : style.grey("\u2502");
4976
+ const divider = `${ERASE_TO_END}${atColumn(chatCols + 1)}${border}`;
4796
4977
  return Array.from(
4797
4978
  { length: rows },
4798
4979
  (_, i) => `${pad(chat[i] ?? "", chatCols)}${divider}${pad(plan[i] ?? "", planCols)}`
4799
4980
  );
4800
4981
  }
4801
4982
  function renderChat(state, rows, cols) {
4802
- const { lines, footer, anchor, maxScroll } = chatLayout(state, rows, cols);
4983
+ const { lines, header, footer, anchor, maxScroll } = chatLayout(state, rows, cols);
4803
4984
  const back = Math.min(state.scroll, maxScroll);
4804
- return [...fit(lines.slice(0, lines.length - back), rows - footer.length, anchor), ...footer];
4985
+ const body = Math.max(0, rows - header.length - footer.length);
4986
+ return [...header, ...fit(lines.slice(0, lines.length - back), body, anchor), ...footer];
4805
4987
  }
4806
4988
  function renderPlan(state, rows, cols) {
4807
4989
  const layout = planLayout(state, rows, cols);
@@ -4867,7 +5049,7 @@ function renderHandoff(state, overlay, rows, cols) {
4867
5049
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
4868
5050
  }
4869
5051
  const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
4870
- const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core22.capConflictFiles)(task.repairedFiles)})`) : "";
5052
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core23.capConflictFiles)(task.repairedFiles)})`) : "";
4871
5053
  return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
4872
5054
  });
4873
5055
  const actions = handoffActions(handoff).map((action, index) => {
@@ -4957,7 +5139,7 @@ function frame(title, body, rows, cols) {
4957
5139
  }
4958
5140
 
4959
5141
  // src/tui/reducer.ts
4960
- var import_core26 = require("@ordewell/core");
5142
+ var import_core29 = require("@ordewell/core");
4961
5143
 
4962
5144
  // src/tui/reducers/shared.ts
4963
5145
  function step(state, effects = []) {
@@ -5006,10 +5188,10 @@ function handleApprovalKey(state, overlay, key) {
5006
5188
  }
5007
5189
 
5008
5190
  // src/tui/reducers/commands.ts
5009
- var import_core25 = require("@ordewell/core");
5191
+ var import_core27 = require("@ordewell/core");
5010
5192
 
5011
5193
  // src/tui/reducers/pickers.ts
5012
- var import_core23 = require("@ordewell/core");
5194
+ var import_core24 = require("@ordewell/core");
5013
5195
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5014
5196
  var DEFAULT_EFFORT = "__runner_default__";
5015
5197
  function picker(title, items, action, extra = {}) {
@@ -5028,11 +5210,11 @@ function modelDetail(m) {
5028
5210
  function providerErrorHint(state) {
5029
5211
  const failed = Object.keys(state.providerErrors ?? {});
5030
5212
  if (failed.length === 0) return void 0;
5031
- const names = failed.map((p) => import_core23.ALL_PROVIDERS[p]?.label ?? p);
5213
+ const names = failed.map((p) => import_core24.ALL_PROVIDERS[p]?.label ?? p);
5032
5214
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5033
5215
  }
5034
5216
  function plannerModelItems(state) {
5035
- const runner = (0, import_core23.runnerForProvider)(state.plannerProvider);
5217
+ const runner = (0, import_core24.runnerForProvider)(state.plannerProvider);
5036
5218
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5037
5219
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5038
5220
  id: m.id,
@@ -5046,7 +5228,7 @@ function plannerModelItems(state) {
5046
5228
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5047
5229
  }
5048
5230
  function plannerEffortItems(state) {
5049
- const runner = (0, import_core23.runnerForProvider)(state.plannerProvider);
5231
+ const runner = (0, import_core24.runnerForProvider)(state.plannerProvider);
5050
5232
  if (!runner) {
5051
5233
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5052
5234
  }
@@ -5155,21 +5337,21 @@ function pickerItemsFor(state, action) {
5155
5337
  }
5156
5338
  function plannerItems(state) {
5157
5339
  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);
5340
+ const agents = import_core24.CLI_PROVIDERS.map((id) => {
5341
+ const runner = (0, import_core24.runnerForProvider)(id);
5160
5342
  const ready = installed.has(runner);
5161
5343
  return {
5162
5344
  id,
5163
- label: import_core23.ALL_PROVIDERS[id].label,
5345
+ label: import_core24.ALL_PROVIDERS[id].label,
5164
5346
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5165
5347
  selected: state.plannerProvider === id,
5166
5348
  disabled: !ready
5167
5349
  };
5168
5350
  });
5169
- const vendors = import_core23.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5351
+ const vendors = import_core24.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5170
5352
  id,
5171
- label: import_core23.ALL_PROVIDERS[id].label,
5172
- detail: import_core23.ALL_PROVIDERS[id].apiKeyEnvVar,
5353
+ label: import_core24.ALL_PROVIDERS[id].label,
5354
+ detail: import_core24.ALL_PROVIDERS[id].apiKeyEnvVar,
5173
5355
  selected: state.plannerProvider === id
5174
5356
  }));
5175
5357
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -5250,6 +5432,139 @@ function handleMouseSelect(state, key) {
5250
5432
  return step({ ...state, selection: null }, [{ type: "copyText", text: selectedText({ ...state, selection: moved }) }]);
5251
5433
  }
5252
5434
 
5435
+ // src/tui/reducers/taskView.ts
5436
+ var import_core25 = require("@ordewell/core");
5437
+ function openTaskView(state, sessionId, taskId) {
5438
+ const task = findTask(state.tasks, taskId);
5439
+ if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5440
+ const taskView = {
5441
+ taskId,
5442
+ view: import_core25.EMPTY_TASK_LOG,
5443
+ attempts: [],
5444
+ attempt: 0,
5445
+ pending: [],
5446
+ loaded: false,
5447
+ followLatest: true,
5448
+ queuedIndex: 0
5449
+ };
5450
+ return step({ ...state, taskView, focus: "chat", scroll: 0 }, [{ type: "openTaskLog", sessionId, taskId }]);
5451
+ }
5452
+ function openTaskTerminalOrView(state, sessionId, taskId) {
5453
+ const task = findTask(state.tasks, taskId);
5454
+ if (task?.transport?.kind === "structured") return openTaskView(state, sessionId, taskId);
5455
+ return step(state, [{ type: "openTaskTerminal", sessionId, taskId }]);
5456
+ }
5457
+ function continueTaskStep(state, sessionId, taskId, text) {
5458
+ const opened = state.taskView?.taskId === taskId ? step({ ...state, taskView: { ...state.taskView, followLatest: true } }) : openTaskView(state, sessionId, taskId);
5459
+ const watch = state.status !== "executing";
5460
+ return step(opened.state, [...opened.effects, { type: "continueTask", sessionId, taskId, text, ...watch ? { watch: true } : {} }]);
5461
+ }
5462
+ function clampQueueIndex(tv) {
5463
+ const max = Math.max(0, tv.view.queued.length - 1);
5464
+ return tv.queuedIndex <= max ? tv : { ...tv, queuedIndex: max };
5465
+ }
5466
+ function applyLive(tv, attempt, events) {
5467
+ if (attempt <= 0) return tv;
5468
+ let next = tv;
5469
+ if (attempt > next.attempt) {
5470
+ if (!next.followLatest) {
5471
+ return next.attempts.includes(attempt) ? next : { ...next, attempts: [...next.attempts, attempt] };
5472
+ }
5473
+ next = {
5474
+ ...next,
5475
+ view: import_core25.EMPTY_TASK_LOG,
5476
+ attempt,
5477
+ attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5478
+ };
5479
+ }
5480
+ if (attempt < next.attempt) return next;
5481
+ return clampQueueIndex({ ...next, view: events.reduce(import_core25.reduceTaskLog, next.view) });
5482
+ }
5483
+ function taskLogAbandoned(state) {
5484
+ const tv = state.taskView;
5485
+ if (!tv || tv.loaded) return state;
5486
+ let next = { ...tv, pending: [], loaded: true };
5487
+ for (const batch of tv.pending) next = applyLive(next, batch.attempt, batch.events);
5488
+ return { ...state, taskView: clampQueueIndex(next) };
5489
+ }
5490
+ function taskLogArrived(state, action) {
5491
+ const tv = state.taskView;
5492
+ if (!tv || tv.taskId !== action.taskId) return state;
5493
+ if (!tv.loaded) {
5494
+ return { ...state, taskView: { ...tv, pending: [...tv.pending, { attempt: action.attempt, events: action.events }] } };
5495
+ }
5496
+ return { ...state, taskView: applyLive(tv, action.attempt, action.events) };
5497
+ }
5498
+ function announceApprovals(state, action) {
5499
+ if (state.taskView?.taskId === action.taskId) return state;
5500
+ const task = findTask(state.tasks, action.taskId);
5501
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core25.runnerToolSubject)(event.tool, event.args)} \u2014 t on it to answer`) : s, state);
5502
+ return state.taskView && spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5503
+ }
5504
+ function taskLogLoaded(state, action) {
5505
+ const tv = state.taskView;
5506
+ if (!tv || tv.taskId !== action.taskId) return state;
5507
+ const loaded = action.attempt >= 1 ? {
5508
+ ...tv,
5509
+ view: (0, import_core25.replayTaskLog)(action.events),
5510
+ attempts: action.attempts ?? tv.attempts,
5511
+ attempt: action.attempt
5512
+ } : { ...tv, attempts: action.attempts ?? tv.attempts };
5513
+ let next = { ...loaded, pending: [], loaded: true };
5514
+ for (const batch of tv.pending) {
5515
+ if (batch.attempt === action.attempt) continue;
5516
+ next = applyLive(next, batch.attempt, batch.events);
5517
+ }
5518
+ return { ...state, taskView: clampQueueIndex(next) };
5519
+ }
5520
+ function answerApproval(state, tv, sessionId, decision) {
5521
+ const approval = waitingApproval(tv);
5522
+ if (!approval?.approvalId) return step(say(state, "system", "Nothing is waiting for approval."));
5523
+ if (decision === "allowForTask" && !approval.allowForTask) {
5524
+ return step(say(state, "system", "The runner offered no grant for the rest of this task \u2014 ctrl-y allows this call."));
5525
+ }
5526
+ const note = decision === "deny" ? state.editor.text.trim() : "";
5527
+ const answer = decision === "deny" ? { decision, ...note ? { note } : {} } : { decision };
5528
+ const next = note ? { ...state, editor: { ...state.editor, text: "", cursor: 0 } } : state;
5529
+ return step(next, [{ type: "answerTaskApproval", sessionId, approvalId: approval.approvalId, answer }]);
5530
+ }
5531
+ function handleTaskViewKey(state, key) {
5532
+ const tv = state.taskView;
5533
+ if (!tv || !state.sessionId) return null;
5534
+ if (key.name === "alt-left" || key.name === "alt-right") {
5535
+ const index = tv.attempts.indexOf(tv.attempt);
5536
+ if (index < 0) return null;
5537
+ const target = key.name === "alt-left" ? index - 1 : index + 1;
5538
+ const attempt = tv.attempts[target];
5539
+ if (attempt === void 0) {
5540
+ return step(say(state, "system", target < 0 ? "This is the first attempt." : "This is the latest attempt."));
5541
+ }
5542
+ return step(
5543
+ { ...state, taskView: { ...tv, followLatest: false } },
5544
+ [{ type: "loadTaskAttempt", sessionId: state.sessionId, taskId: tv.taskId, attempt }]
5545
+ );
5546
+ }
5547
+ if (key.name === "ctrl-r") {
5548
+ const queued = tv.view.queued[tv.queuedIndex];
5549
+ if (!queued) return step(say(state, "system", "No queued message to remove."));
5550
+ return step(state, [{ type: "removeTaskMessage", sessionId: state.sessionId, taskId: tv.taskId, messageId: queued.id }]);
5551
+ }
5552
+ if (key.name === "ctrl-x") {
5553
+ return step(state, [{ type: "interruptTask", sessionId: state.sessionId, taskId: tv.taskId }]);
5554
+ }
5555
+ if (key.name === "ctrl-y") return answerApproval(state, tv, state.sessionId, "allow");
5556
+ if (key.name === "ctrl-t") return answerApproval(state, tv, state.sessionId, "allowForTask");
5557
+ if (key.name === "ctrl-g") return answerApproval(state, tv, state.sessionId, "deny");
5558
+ if (key.name === "ctrl-n" || key.name === "ctrl-p") {
5559
+ const count = tv.view.queued.length;
5560
+ if (count === 0) return null;
5561
+ const delta = key.name === "ctrl-n" ? 1 : -1;
5562
+ const queuedIndex = Math.max(0, Math.min(count - 1, tv.queuedIndex + delta));
5563
+ return step({ ...state, taskView: { ...tv, queuedIndex } });
5564
+ }
5565
+ return null;
5566
+ }
5567
+
5253
5568
  // src/tui/reducers/taskEdits.ts
5254
5569
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
5255
5570
  function openTaskRunnerPicker(state, task) {
@@ -5520,9 +5835,7 @@ function handlePlanKey(state, key) {
5520
5835
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
5521
5836
  }
5522
5837
  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 }]);
5525
- }
5838
+ if (key.char === "t") return openTaskTerminalOrView(state, state.sessionId, task.id);
5526
5839
  if (key.char === "x" && task.isolation?.state === "conflict") {
5527
5840
  return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
5528
5841
  }
@@ -5555,9 +5868,9 @@ function commitTaskEdit(state, task, editor) {
5555
5868
  }
5556
5869
 
5557
5870
  // src/tui/reducers/turnQueue.ts
5558
- var import_core24 = require("@ordewell/core");
5871
+ var import_core26 = require("@ordewell/core");
5559
5872
  function drainQueue(state, settled) {
5560
- const next = (0, import_core24.drainNext)(state.queuedPrompts);
5873
+ const next = (0, import_core26.drainNext)(state.queuedPrompts);
5561
5874
  const ended = disarmStop({ ...settled, stopRequested: false });
5562
5875
  if (!next) return step(ended);
5563
5876
  const spoken = say(ended, "user", next.text);
@@ -5576,14 +5889,14 @@ function stopPlanning(state, sessionId) {
5576
5889
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
5577
5890
  }
5578
5891
  function queueToEditor(state) {
5579
- const all = (0, import_core24.unsendAll)(state.queuedPrompts);
5892
+ const all = (0, import_core26.unsendAll)(state.queuedPrompts);
5580
5893
  if (!all) return state;
5581
5894
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
5582
5895
  }
5583
5896
  function unsendLatest(state) {
5584
- const latest = (0, import_core24.unsendLatest)(state.queuedPrompts);
5897
+ const latest = (0, import_core26.unsendLatest)(state.queuedPrompts);
5585
5898
  if (!latest) return step(state);
5586
- const text = (0, import_core24.aheadOfDraft)(latest.text, state.editor.text);
5899
+ const text = (0, import_core26.aheadOfDraft)(latest.text, state.editor.text);
5587
5900
  return step({
5588
5901
  ...state,
5589
5902
  queuedPrompts: latest.rest,
@@ -5600,7 +5913,7 @@ function plannerEscape(state) {
5600
5913
  }
5601
5914
 
5602
5915
  // src/tui/reducers/commands.ts
5603
- var KNOWN_PROVIDERS2 = Object.keys(import_core25.ALL_PROVIDERS);
5916
+ var KNOWN_PROVIDERS2 = Object.keys(import_core27.ALL_PROVIDERS);
5604
5917
  function runCommand(state, { name, args }) {
5605
5918
  if (!findCommand(name)) {
5606
5919
  return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
@@ -5641,6 +5954,8 @@ function runCommand(state, { name, args }) {
5641
5954
  return runners(state, args);
5642
5955
  case "auto":
5643
5956
  return setAutonomous(state, args[0]);
5957
+ case "transport":
5958
+ return setTransport(state, args[0]);
5644
5959
  case "parallel":
5645
5960
  return setMaxParallel(state, args[0]);
5646
5961
  case "mouse":
@@ -5678,11 +5993,13 @@ function runCommand(state, { name, args }) {
5678
5993
  (sessionId, taskId) => step(state, [{ type: "removeTask", sessionId, taskId }])
5679
5994
  );
5680
5995
  case "terminal":
5681
- return taskCommand(
5682
- state,
5683
- args[0],
5684
- (sessionId, taskId) => step(state, [{ type: "openTaskTerminal", sessionId, taskId }])
5685
- );
5996
+ return taskCommand(state, args[0], (sessionId, taskId) => openTaskTerminalOrView(state, sessionId, taskId));
5997
+ case "continue":
5998
+ return taskCommand(state, args[0], (sessionId, taskId) => {
5999
+ const text = args.slice(1).join(" ").trim();
6000
+ if (!text) return fail3(state, "Usage: /continue <id> <message>");
6001
+ return continueTaskStep(state, sessionId, taskId, text);
6002
+ });
5686
6003
  case "task-runner":
5687
6004
  return taskRunnerCommand(state, args);
5688
6005
  case "task-model":
@@ -5749,10 +6066,11 @@ function newSession(state) {
5749
6066
  handoff: null,
5750
6067
  sessionId: null,
5751
6068
  goal: "",
6069
+ taskView: null,
5752
6070
  tasks: [],
5753
6071
  planApproved: false,
5754
- conversation: import_core25.EMPTY_CONVERSATION,
5755
- turnGate: import_core25.NO_TURN,
6072
+ conversation: import_core27.EMPTY_CONVERSATION,
6073
+ turnGate: import_core27.NO_TURN,
5756
6074
  selectedTask: 0,
5757
6075
  expandedTaskId: null,
5758
6076
  taskEditor: null,
@@ -5763,14 +6081,14 @@ function newSession(state) {
5763
6081
  // Prompts belong to the session that raised them; the old planner is gone
5764
6082
  // and its pending requests deny on their own timeout.
5765
6083
  pendingApprovals: [],
5766
- queuedPrompts: import_core25.EMPTY_HOLD,
6084
+ queuedPrompts: import_core27.EMPTY_HOLD,
5767
6085
  stopArmed: false,
5768
6086
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
5769
6087
  }, closeEffects);
5770
6088
  }
5771
6089
  function setModel(state, args) {
5772
6090
  if (args[0] === "set" && args[1]) {
5773
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
6091
+ const runner = (0, import_core27.runnerForProvider)(state.plannerProvider);
5774
6092
  const known = runner ? modelsForRunner(state.models, runner) : [];
5775
6093
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
5776
6094
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -5843,16 +6161,16 @@ function setKey(state, args) {
5843
6161
  }
5844
6162
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
5845
6163
  }
5846
- const items = import_core25.PROVIDER_PRIORITY.map((id) => ({
6164
+ const items = import_core27.PROVIDER_PRIORITY.map((id) => ({
5847
6165
  id,
5848
- label: import_core25.ALL_PROVIDERS[id].label,
5849
- detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
6166
+ label: import_core27.ALL_PROVIDERS[id].label,
6167
+ detail: import_core27.ALL_PROVIDERS[id].apiKeyEnvVar,
5850
6168
  selected: state.configuredProviders.includes(id)
5851
6169
  }));
5852
6170
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
5853
6171
  }
5854
6172
  function keyPrompt(provider) {
5855
- const meta = import_core25.ALL_PROVIDERS[provider];
6173
+ const meta = import_core27.ALL_PROVIDERS[provider];
5856
6174
  return {
5857
6175
  kind: "prompt",
5858
6176
  title: `${meta.label} API key`,
@@ -5929,7 +6247,7 @@ function runners(state, args) {
5929
6247
  }
5930
6248
  function setMaxParallel(state, arg) {
5931
6249
  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);
6250
+ const limit = (0, import_core27.parseMaxParallel)(arg);
5933
6251
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
5934
6252
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
5935
6253
  }
@@ -5938,6 +6256,11 @@ function setAutonomous(state, arg) {
5938
6256
  if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
5939
6257
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
5940
6258
  }
6259
+ function setTransport(state, arg) {
6260
+ const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6261
+ if (!(0, import_core27.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6262
+ return step(state, [{ type: "setTransport", transport: value }]);
6263
+ }
5941
6264
  function setMouseCapture(state, arg) {
5942
6265
  const enabled = resolveToggle(arg, state.mouseCapture);
5943
6266
  if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
@@ -6185,6 +6508,7 @@ function choose(state, picker2, item) {
6185
6508
  }
6186
6509
 
6187
6510
  // src/tui/reducers/incoming.ts
6511
+ var import_core28 = require("@ordewell/core");
6188
6512
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6189
6513
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6190
6514
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6215,6 +6539,7 @@ function toTaskView2(t, index) {
6215
6539
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6216
6540
  type: t.type === "user" ? "user" : "ai",
6217
6541
  status: planLabel(t.status, "pending"),
6542
+ awaitingReason: t.status === "awaiting_user" && (0, import_core28.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6218
6543
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6219
6544
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6220
6545
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6245,6 +6570,7 @@ function applySettings(state, settings) {
6245
6570
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6246
6571
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6247
6572
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6573
+ runnerTransport: (0, import_core28.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6248
6574
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6249
6575
  };
6250
6576
  }
@@ -6279,6 +6605,7 @@ function followSession(state, message) {
6279
6605
  const heard = turnActivity(hear(state, message));
6280
6606
  if (message.type === "approval_request") {
6281
6607
  const { id, kind, subject, scope, detail } = message;
6608
+ if (kind === "runner_tool") return heard;
6282
6609
  return enqueueApproval(heard, { id, kind, subject, scope, ...detail ? { detail } : {} });
6283
6610
  }
6284
6611
  return message.type === "approval_settled" ? dropApproval(heard, message.id) : heard;
@@ -6292,7 +6619,7 @@ function reduce(state, action) {
6292
6619
  // Isolation belongs to one session's plan, and task ids repeat across
6293
6620
  // sessions, so nothing of the last one's may carry over into this.
6294
6621
  case "sessionStarted":
6295
- return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
6622
+ return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal, taskView: null }));
6296
6623
  // The daemon registers a session only once planning succeeds; when it does
6297
6624
  // not, holding on to the id would send every next message to a session the
6298
6625
  // server never had.
@@ -6301,14 +6628,15 @@ function reduce(state, action) {
6301
6628
  ...clearIsolation(state),
6302
6629
  sessionId: null,
6303
6630
  goal: "",
6631
+ taskView: null,
6304
6632
  pendingApprovals: [],
6305
- queuedPrompts: import_core26.EMPTY_HOLD,
6633
+ queuedPrompts: import_core29.EMPTY_HOLD,
6306
6634
  stopArmed: false,
6307
6635
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6308
6636
  });
6309
6637
  case "chatRestored":
6310
6638
  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 });
6639
+ return step({ ...state, conversation: (0, import_core29.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6312
6640
  case "planUpdated": {
6313
6641
  if (stale(state, action.sessionId)) return step(state);
6314
6642
  const isolation = isolationOfPlan(action.plan);
@@ -6340,6 +6668,12 @@ function reduce(state, action) {
6340
6668
  case "sessionMessage":
6341
6669
  if (stale(state, action.sessionId)) return step(state);
6342
6670
  return step(followSession(state, action.message));
6671
+ case "taskLog":
6672
+ if (stale(state, action.sessionId)) return step(state);
6673
+ return step(announceApprovals(taskLogArrived(state, action), action));
6674
+ case "taskLogLoaded":
6675
+ if (stale(state, action.sessionId)) return step(state);
6676
+ return step(taskLogLoaded(state, action));
6343
6677
  // A settled transcript line, not a research step: nothing ever settles a
6344
6678
  // task's start, so as a step it stayed "⋯" forever and was counted into
6345
6679
  // the planner's next "(+N more)".
@@ -6348,12 +6682,12 @@ function reduce(state, action) {
6348
6682
  const existing = findTask(state.tasks, action.taskId);
6349
6683
  if (existing && isTaskRunning(existing)) return step(state);
6350
6684
  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));
6685
+ const spoken = say(state, "system", (0, import_core29.taskStartedNotice)(action.title, action.runner));
6352
6686
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
6353
6687
  }
6354
6688
  case "taskStatus": {
6355
6689
  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);
6690
+ const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0 } : t);
6357
6691
  const status = runStatus(state, tasks);
6358
6692
  return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
6359
6693
  }
@@ -6366,9 +6700,13 @@ function reduce(state, action) {
6366
6700
  if (update === void 0) return t;
6367
6701
  const idleSince = update.idleSince ?? null;
6368
6702
  const isolation = update.isolation ?? t.isolation;
6369
- if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
6703
+ const transport = update.transport;
6704
+ const awaitingReason = update.awaitingReason;
6705
+ const continuable = update.continuable === true;
6706
+ const awaitingApproval = update.awaitingApproval;
6707
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || !sameTransport(transport, t.transport) || awaitingReason !== t.awaitingReason || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval) {
6370
6708
  changed = true;
6371
- return { ...t, status: update.status, idleSince, isolation };
6709
+ return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval };
6372
6710
  }
6373
6711
  return t;
6374
6712
  });
@@ -6443,11 +6781,12 @@ function reduce(state, action) {
6443
6781
  ...clearIsolation(state),
6444
6782
  sessionId: action.sessionId,
6445
6783
  goal: action.goal,
6784
+ taskView: null,
6446
6785
  status: "idle",
6447
6786
  busyLabel: "",
6448
6787
  planApproved: false,
6449
6788
  pendingApprovals: [],
6450
- queuedPrompts: import_core26.EMPTY_HOLD,
6789
+ queuedPrompts: import_core29.EMPTY_HOLD,
6451
6790
  stopArmed: false,
6452
6791
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6453
6792
  });
@@ -6467,6 +6806,7 @@ function reduce(state, action) {
6467
6806
  orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
6468
6807
  }, ["set-planner", "set-task-runner"]));
6469
6808
  case "failed": {
6809
+ state = taskLogAbandoned(state);
6470
6810
  const quiet = plannerInFlight(state) && state.stopRequested;
6471
6811
  const reported = { ...quiet ? state : say(state, "error", action.message), status: "idle", busyLabel: "" };
6472
6812
  if (!plannerInFlight(state)) return step(reported);
@@ -6502,6 +6842,9 @@ function reduce(state, action) {
6502
6842
  return step(disarmStop(state));
6503
6843
  }
6504
6844
  }
6845
+ function sameTransport(a, b) {
6846
+ return a?.kind === b?.kind && a?.fallback === b?.fallback;
6847
+ }
6505
6848
  function runStatus(state, tasks) {
6506
6849
  if (tasks.some(isTaskRunning)) return "executing";
6507
6850
  if (tasks.some((t) => t.status === "awaiting_user")) return "executing";
@@ -6520,6 +6863,10 @@ function handleKey2(state, key) {
6520
6863
  }
6521
6864
  if (key.name === "ctrl-l") return step({ ...state, conversation: wiped(state.conversation), scroll: 0 });
6522
6865
  if (key.name === "ctrl-o" && state.focus === "chat" && !state.overlay) return toggleDetail(state);
6866
+ if (key.name === "escape" && state.taskView && !state.overlay) {
6867
+ if (state.editor.text) return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
6868
+ return step({ ...state, taskView: null, scroll: 0 });
6869
+ }
6523
6870
  if (key.name === "escape") {
6524
6871
  const stopped = plannerEscape(state);
6525
6872
  if (stopped) return stopped;
@@ -6543,12 +6890,16 @@ function handleKey2(state, key) {
6543
6890
  return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
6544
6891
  }
6545
6892
  if (state.focus === "plan") return handlePlanKey(state, key);
6893
+ if (state.taskView) {
6894
+ const handled = handleTaskViewKey(state, key);
6895
+ if (handled) return handled;
6896
+ }
6546
6897
  if (key.name === "enter") return submit(state);
6547
6898
  if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
6548
6899
  const scroll = scrollDelta(key, state);
6549
6900
  if (scroll !== null) return scrollChat(state, scroll);
6550
6901
  const multilineEditor = state.editor.text.includes("\n");
6551
- const room = chatEditorRoom(state.cols, true);
6902
+ const room = chatEditorRoomFor(state, true);
6552
6903
  return step({ ...state, editor: applyKey(state.editor, key, multilineEditor ? room : void 0) });
6553
6904
  }
6554
6905
  function submit(state) {
@@ -6557,8 +6908,14 @@ function submit(state) {
6557
6908
  const cleared = { ...state, editor: commit(state.editor) };
6558
6909
  const command = parseSlash(text);
6559
6910
  if (command && findCommand(command.name)?.source !== "skill") return runCommand(cleared, command);
6911
+ if (state.taskView) {
6912
+ if (!state.sessionId) return step(state);
6913
+ const taskId = state.taskView.taskId;
6914
+ if (continuesTask(findTask(state.tasks, taskId))) return continueTaskStep(cleared, state.sessionId, taskId, text);
6915
+ return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
6916
+ }
6560
6917
  if (plannerInFlight(state)) {
6561
- return step({ ...cleared, queuedPrompts: (0, import_core26.holdPrompt)(state.queuedPrompts, text) });
6918
+ return step({ ...cleared, queuedPrompts: (0, import_core29.holdPrompt)(state.queuedPrompts, text) });
6562
6919
  }
6563
6920
  const spoken = say(cleared, "user", text);
6564
6921
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -6618,10 +6975,10 @@ function createApp(deps) {
6618
6975
 
6619
6976
  // src/tui/effects.ts
6620
6977
  var import_child_process4 = require("child_process");
6621
- var import_core28 = require("@ordewell/core");
6978
+ var import_core31 = require("@ordewell/core");
6622
6979
 
6623
6980
  // src/tui/inbound.ts
6624
- var import_core27 = require("@ordewell/core");
6981
+ var import_core30 = require("@ordewell/core");
6625
6982
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
6626
6983
  "planner_message",
6627
6984
  "planner_turn_started",
@@ -6831,7 +7188,15 @@ function dispatchLifecycle(dispatch, event, sessionId) {
6831
7188
  case "status_update": {
6832
7189
  const updates = {};
6833
7190
  for (const task of event.tasks ?? []) {
6834
- updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
7191
+ updates[String(task.id)] = {
7192
+ status: String(task.status),
7193
+ idleSince: task.idleSince ?? null,
7194
+ isolation: task.isolation,
7195
+ transport: task.transport,
7196
+ awaitingReason: (0, import_core30.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7197
+ ...task.continuable ? { continuable: true } : {},
7198
+ awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0
7199
+ };
6835
7200
  }
6836
7201
  dispatch({ type: "tasksStatus", updates, sessionId });
6837
7202
  return;
@@ -6842,7 +7207,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
6842
7207
  // The extension shows these in its checkpoint panel; here they are
6843
7208
  // transcript lines, which is the TUI's equivalent surface.
6844
7209
  case "checkpoint":
6845
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core27.truncateCheckpointSummary)(event.summary)}` });
7210
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core30.truncateCheckpointSummary)(event.summary)}` });
6846
7211
  return;
6847
7212
  case "review_needed":
6848
7213
  dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
@@ -6875,6 +7240,12 @@ function dispatchLifecycle(dispatch, event, sessionId) {
6875
7240
  case "isolation_handoff":
6876
7241
  dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
6877
7242
  return;
7243
+ // A structured task's log as it happens (ADR-0018, P1): the task view is
7244
+ // its only reader, and the reducer drops it unless that view is open on
7245
+ // this task. The run's watcher itself does nothing with it.
7246
+ case "task_log":
7247
+ dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, sessionId });
7248
+ return;
6878
7249
  // Raw runner chatter. Listed rather than defaulted, so a new lifecycle
6879
7250
  // variant fails to compile here until someone decides what a run's watcher
6880
7251
  // does with it.
@@ -7025,17 +7396,73 @@ async function perform(effect, deps) {
7025
7396
  }
7026
7397
  return;
7027
7398
  }
7399
+ // The card settles when the task log reports the answer, not here.
7400
+ case "answerTaskApproval": {
7401
+ try {
7402
+ await api.respondToApproval(effect.sessionId, effect.approvalId, effect.answer);
7403
+ } catch {
7404
+ dispatch({ type: "failed", message: "Could not deliver the answer \u2014 the task is still waiting for approval." });
7405
+ }
7406
+ return;
7407
+ }
7028
7408
  case "openTaskTerminal": {
7029
7409
  const result = await deps.openTerminal(effect.sessionId, effect.taskId);
7030
7410
  dispatch(result.ok ? { type: "notice", message: result.message } : { type: "failed", message: result.message });
7031
7411
  return;
7032
7412
  }
7413
+ // The saved log is read off disk (ADR-0018, P1) so the view opens with the
7414
+ // task's history; the live `task_log` stream then catches it up in place.
7415
+ case "openTaskLog": {
7416
+ const attempts = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
7417
+ if (attempts.length === 0) {
7418
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], sessionId: effect.sessionId });
7419
+ return;
7420
+ }
7421
+ const attempt = attempts[attempts.length - 1];
7422
+ const events = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
7423
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, attempt, events, sessionId: effect.sessionId });
7424
+ return;
7425
+ }
7426
+ case "loadTaskAttempt": {
7427
+ const events = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
7428
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempt: effect.attempt, events, sessionId: effect.sessionId });
7429
+ return;
7430
+ }
7431
+ // The daemon queues the message (or delivers it to a waiting task) and
7432
+ // broadcasts the `task_log` event that shows it; nothing to dispatch here.
7433
+ case "sendTaskMessage":
7434
+ await api.sendTaskMessage(effect.sessionId, effect.taskId, effect.text);
7435
+ return;
7436
+ // The daemon starts the new attempt; its `task_log` and status arrive over
7437
+ // the execution stream, which a TUI not already watching a run holds open.
7438
+ case "continueTask": {
7439
+ const { sessionId, taskId, text } = effect;
7440
+ const request = () => api.continueTask(sessionId, taskId, text);
7441
+ await (effect.watch ? withExecutionStream(deps, sessionId, request) : request());
7442
+ await refreshPlan(deps, sessionId);
7443
+ return;
7444
+ }
7445
+ case "removeTaskMessage": {
7446
+ const { removed } = await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
7447
+ if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
7448
+ return;
7449
+ }
7450
+ case "interruptTask":
7451
+ await api.interruptTask(effect.sessionId, effect.taskId);
7452
+ dispatch({ type: "notice", message: "Interrupting the task\u2026" });
7453
+ return;
7033
7454
  case "command": {
7034
7455
  const result = await api.sendCommand(effect.name, { action: effect.action });
7035
7456
  if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7036
7457
  dispatch({ type: "notice", message: `${effect.name} is ${effect.action}.` });
7037
7458
  return;
7038
7459
  }
7460
+ case "setTransport": {
7461
+ const result = await api.sendCommand("transport", { action: effect.transport });
7462
+ if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7463
+ dispatch({ type: "notice", message: `Runner transport is ${effect.transport} (experimental) \u2014 it applies from the next run.` });
7464
+ return;
7465
+ }
7039
7466
  case "setModel":
7040
7467
  await api.updateSettings({ orchestratorModel: effect.modelId });
7041
7468
  deps.setEnvVar("ORCHESTRATOR_MODEL", effect.modelId);
@@ -7043,7 +7470,7 @@ async function perform(effect, deps) {
7043
7470
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7044
7471
  return;
7045
7472
  case "setPlanner": {
7046
- const meta = import_core28.ALL_PROVIDERS[effect.provider];
7473
+ const meta = import_core31.ALL_PROVIDERS[effect.provider];
7047
7474
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7048
7475
  const env = { AI_PROVIDER: effect.provider };
7049
7476
  const settings = await api.updateSettings({ env });
@@ -7055,7 +7482,7 @@ async function perform(effect, deps) {
7055
7482
  });
7056
7483
  dispatch({
7057
7484
  type: "notice",
7058
- message: `${(0, import_core28.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7485
+ message: `${(0, import_core31.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7059
7486
  });
7060
7487
  await loadModels(deps);
7061
7488
  return;
@@ -7067,7 +7494,7 @@ async function perform(effect, deps) {
7067
7494
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7068
7495
  return;
7069
7496
  case "setApiKey": {
7070
- const meta = import_core28.ALL_PROVIDERS[effect.provider];
7497
+ const meta = import_core31.ALL_PROVIDERS[effect.provider];
7071
7498
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7072
7499
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7073
7500
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7238,7 +7665,7 @@ function osc52(text) {
7238
7665
  function copySelection(deps, text) {
7239
7666
  if (!text) return;
7240
7667
  const lines = text.split("\n").length;
7241
- const command = (0, import_core28.clipboardCopyCommand)(deps.hasBin);
7668
+ const command = (0, import_core31.clipboardCopyCommand)(deps.hasBin);
7242
7669
  if (command) {
7243
7670
  try {
7244
7671
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -7348,13 +7775,18 @@ var CONTROL2 = {
7348
7775
  "": "ctrl-c",
7349
7776
  "": "ctrl-d",
7350
7777
  "": "ctrl-e",
7778
+ "\x07": "ctrl-g",
7351
7779
  "\v": "ctrl-k",
7352
7780
  "\f": "ctrl-l",
7353
7781
  "": "ctrl-n",
7354
7782
  "": "ctrl-o",
7355
7783
  "": "ctrl-p",
7784
+ "": "ctrl-r",
7785
+ "": "ctrl-t",
7356
7786
  "": "ctrl-u",
7357
- "": "ctrl-w"
7787
+ "": "ctrl-w",
7788
+ "": "ctrl-x",
7789
+ "": "ctrl-y"
7358
7790
  };
7359
7791
  var ESCAPES = {
7360
7792
  "\x1B[A": "up",
@@ -7623,7 +8055,7 @@ async function handleTui(subArgs) {
7623
8055
  }
7624
8056
  const workspace = flag(subArgs, "--workspace") || process.cwd();
7625
8057
  try {
7626
- (0, import_core29.assertWorkspaceExists)(workspace);
8058
+ (0, import_core32.assertWorkspaceExists)(workspace);
7627
8059
  } catch {
7628
8060
  console.error(
7629
8061
  `Workspace "${workspace}" does not exist.
@@ -7632,7 +8064,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7632
8064
  );
7633
8065
  process.exit(1);
7634
8066
  }
7635
- registerSkillCommands((0, import_core29.createSkillsService)(workspace).listSkills());
8067
+ registerSkillCommands((0, import_core32.createSkillsService)(workspace).listSkills());
7636
8068
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
7637
8069
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
7638
8070
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -7649,7 +8081,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7649
8081
  workspace,
7650
8082
  port,
7651
8083
  dispatch: (action) => app.dispatch(action),
7652
- newSessionId: () => (0, import_core29.mintSessionId)(),
8084
+ newSessionId: () => (0, import_core32.mintSessionId)(),
7653
8085
  setEnvVar: (key, value) => {
7654
8086
  writeEnvVar(findEnvFile(), key, value);
7655
8087
  process.env[key] = value;
@@ -7725,6 +8157,7 @@ var COMMANDS = {
7725
8157
  key: handleKey,
7726
8158
  runners: handleRunners,
7727
8159
  auto: handleAuto,
8160
+ transport: handleTransport,
7728
8161
  refresh: handleRefresh,
7729
8162
  setup: handleSetup,
7730
8163
  plugins: handlePlugins,
@@ -7738,6 +8171,7 @@ var COMMANDS = {
7738
8171
  "force-start": handleForceStart,
7739
8172
  "retry": handleRetry,
7740
8173
  "cancel": handleCancel,
8174
+ continue: handleContinue,
7741
8175
  "add-task": handleAddTask,
7742
8176
  "remove-task": handleRemoveTask,
7743
8177
  "task-runner": handleTaskRunner,
@@ -7770,7 +8204,7 @@ function cliVersion() {
7770
8204
  // src/main.ts
7771
8205
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
7772
8206
  async function main() {
7773
- (0, import_core30.migrateOldConfigDir)();
8207
+ (0, import_core33.migrateOldConfigDir)();
7774
8208
  loadEnvFile();
7775
8209
  const argv = process.argv.slice(2);
7776
8210
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");