@ordewell/cli 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/main.js +658 -201
  3. 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_core34 = 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
 
@@ -123,7 +138,8 @@ Planner, models and runners:
123
138
  ordewell key [set <p> <key>] Show which providers have a key, or store one
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
- ordewell auto [on|off] Autonomous permission mode for new sessions
141
+ ordewell auto [full|auto] Autonomy level for new sessions: Full auto or Auto
142
+ ordewell transport [terminal|structured] Structured (default) or terminal task transport, 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)
@@ -196,7 +212,7 @@ Environment:
196
212
  ORDEWELL_PLANNER_EFFORT Thinking effort for a coding-agent planner
197
213
  ORCHESTRATOR_MODEL Default: deepseek/deepseek-v4-flash
198
214
  OPENROUTER_BASE_URL Default: https://openrouter.ai/api/v1
199
- ORDEWELL_AUTONOMOUS_MODE Approval posture for new sessions (see "ordewell auto")
215
+ ORDEWELL_AUTONOMOUS_MODE true: Full auto, false: Auto, for new sessions (see "ordewell auto")
200
216
  ORDEWELL_PORT Daemon port CLI commands target (default: 3742)
201
217
  ORDEWELL_DIRENV false: agents skip the project's direnv .envrc
202
218
  ORDEWELL_RESEARCH_ENABLED true (default) or false
@@ -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: ${transport === "terminal" ? "terminal" : "structured"} \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,24 +2617,24 @@ 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;
2522
- const scroll = conversation.nextId > state.conversation.nextId ? 0 : state.scroll;
2523
- return { ...gated, conversation, scroll };
2624
+ return { ...gated, conversation };
2524
2625
  }
2525
2626
  function fold(state, input) {
2526
- return show(state, { view: (0, import_core14.reduceConversation)(state.conversation, input), gate: state.turnGate });
2627
+ return show(state, { view: (0, import_core15.reduceConversation)(state.conversation, input), gate: state.turnGate });
2527
2628
  }
2528
2629
  function say(state, role, text) {
2529
- return fold(state, { type: "local_entry", role, text });
2630
+ const said = fold(state, { type: "local_entry", role, text });
2631
+ return said.conversation.nextId > state.conversation.nextId ? { ...said, scroll: 0 } : said;
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
  }
@@ -3100,6 +3208,7 @@ async function handleKey(subArgs, injectedApi) {
3100
3208
  }
3101
3209
 
3102
3210
  // src/commands/runners.ts
3211
+ var import_core19 = require("@ordewell/core");
3103
3212
  var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
3104
3213
  async function handleRunners(subArgs, injectedApi) {
3105
3214
  const [runner, toggle] = positionals(subArgs);
@@ -3124,21 +3233,21 @@ async function handleRunners(subArgs, injectedApi) {
3124
3233
  await api.setRunnerEnabled(runner, enabled);
3125
3234
  console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
3126
3235
  }
3127
- var AUTO_USAGE = "Usage: ordewell auto [on|off]";
3236
+ var AUTO_USAGE = "Usage: ordewell auto [full|auto]";
3128
3237
  function handleAuto(subArgs) {
3129
3238
  const [arg] = positionals(subArgs);
3130
3239
  const currentRaw = process.env.ORDEWELL_AUTONOMOUS_MODE;
3131
3240
  const current = currentRaw !== "false" && currentRaw !== "0";
3132
3241
  if (!arg) {
3133
- console.log(`Autonomous mode: ${current ? "ON" : "OFF"}`);
3242
+ console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(current)}`);
3134
3243
  console.log(` ${AUTO_USAGE}`);
3135
3244
  return;
3136
3245
  }
3137
- const enabled = arg === "on" ? true : arg === "off" ? false : null;
3246
+ const enabled = (0, import_core19.parseAutonomyLevel)(arg);
3138
3247
  if (enabled === null) fail(AUTO_USAGE);
3139
3248
  writeEnvVar(findEnvFile(), "ORDEWELL_AUTONOMOUS_MODE", String(enabled));
3140
3249
  process.env.ORDEWELL_AUTONOMOUS_MODE = String(enabled);
3141
- console.log(`Autonomous mode ${enabled ? "on" : "off"} for new sessions.`);
3250
+ console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(enabled)} for new sessions.`);
3142
3251
  }
3143
3252
  async function handleRefresh(subArgs, injectedApi) {
3144
3253
  const api = await connect(subArgs, injectedApi);
@@ -3170,7 +3279,7 @@ async function handleApprove(subArgs, injectedApi) {
3170
3279
  // src/tui/terminalLauncher.ts
3171
3280
  var import_child_process3 = require("child_process");
3172
3281
  var import_util = require("util");
3173
- var import_core18 = require("@ordewell/core");
3282
+ var import_core20 = require("@ordewell/core");
3174
3283
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3175
3284
  var defaultExecFile = async (command, args) => {
3176
3285
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3190,16 +3299,16 @@ var defaultWhich = (bin) => {
3190
3299
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3191
3300
  const platform = deps.platform ?? process.platform;
3192
3301
  const env = deps.env ?? process.env;
3193
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core18.hasTmux;
3302
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core20.hasTmux;
3194
3303
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3195
3304
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3196
3305
  const which = deps.which ?? defaultWhich;
3197
3306
  if (!hasTmuxImpl()) {
3198
- return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
3307
+ return { ok: false, message: "No terminal window for this task: tmux is not installed. Install tmux to get one for terminal-transport tasks; a structured task has its log view instead." };
3199
3308
  }
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);
3309
+ const session = (0, import_core20.tmuxSessionName)(port);
3310
+ const socket = (0, import_core20.tmuxSocketName)(port);
3311
+ const base = (0, import_core20.tmuxWindowName)(taskId, planSessionId);
3203
3312
  let windows;
3204
3313
  try {
3205
3314
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3209,7 +3318,7 @@ async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3209
3318
  }
3210
3319
  const window = latestAttempt(windows, base);
3211
3320
  if (!window) {
3212
- return { ok: false, message: "This task hasn't opened a terminal yet \u2014 it may still be pending." };
3321
+ return { ok: false, message: "This task has no live terminal and no saved log \u2014 it may still be pending." };
3213
3322
  }
3214
3323
  const manualHint = `tmux -L ${socket} attach -t ${session}:${window}`;
3215
3324
  const viewer = `${session}-view-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -3493,7 +3602,7 @@ Dependencies \xB7 #${task.order} ${task.title}
3493
3602
  }
3494
3603
 
3495
3604
  // src/tui/index.ts
3496
- var import_core29 = require("@ordewell/core");
3605
+ var import_core33 = require("@ordewell/core");
3497
3606
 
3498
3607
  // src/tui/editor.ts
3499
3608
  function emptyEditor() {
@@ -3641,7 +3750,8 @@ var SLASH_COMMANDS = [
3641
3750
  { name: "retry", usage: "/retry <id>", description: "Re-run a failed task", category: "tasks" },
3642
3751
  { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
3643
3752
  { 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" },
3753
+ { name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
3754
+ { name: "terminal", usage: "/terminal <id>", description: "Open a task: its OS terminal, or a structured task's live log", category: "tasks" },
3645
3755
  { 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
3756
  { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
3647
3757
  { name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
@@ -3654,7 +3764,8 @@ var SLASH_COMMANDS = [
3654
3764
  { name: "key", usage: "/key [set <provider> <key>]", description: "Configure API provider keys", category: "models" },
3655
3765
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3656
3766
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3657
- { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3767
+ { name: "auto", usage: "/auto [full|auto]", description: "Autonomy level for new plans: Full auto or Auto", category: "models" },
3768
+ { name: "transport", usage: "/transport [terminal|structured]", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run", category: "models" },
3658
3769
  { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3659
3770
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3660
3771
  // Skills
@@ -3739,7 +3850,7 @@ function tokenCompletions(token) {
3739
3850
  }
3740
3851
 
3741
3852
  // src/tui/blocks.ts
3742
- var import_core19 = require("@ordewell/core");
3853
+ var import_core21 = require("@ordewell/core");
3743
3854
 
3744
3855
  // src/tui/markdown.ts
3745
3856
  function renderMarkdown(source, cols) {
@@ -3872,13 +3983,14 @@ function tailOf(text, max) {
3872
3983
  var ROLE_PREFIX = {
3873
3984
  user: (t) => `${style.cyan("\u276F")} ${t}`,
3874
3985
  planner: (t) => `${style.magenta("\u25C6")} ${t}`,
3986
+ agent: (t) => `${style.magenta("\u25C6")} ${t}`,
3875
3987
  system: (t) => style.grey(`\xB7 ${t}`),
3876
3988
  error: (t) => `${style.red("\u2717")} ${style.red(t)}`
3877
3989
  };
3878
3990
  function messageLines(block, cols) {
3879
3991
  const text = sanitize(block.text);
3880
3992
  const room = Math.max(1, cols - 2);
3881
- const wrapped = block.role === "planner" ? renderMarkdown(text, room) : wrap(text, room);
3993
+ const wrapped = block.role === "planner" || block.role === "agent" ? renderMarkdown(text, room) : wrap(text, room);
3882
3994
  return wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[block.role](line) : ` ${line}`);
3883
3995
  }
3884
3996
  function thinkingLines(block, cols, detailAll) {
@@ -3946,16 +4058,16 @@ function resultLines(block, room, detailAll) {
3946
4058
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
3947
4059
  const outcome = unsettledOutcome(block);
3948
4060
  if (outcome) {
3949
- const [reason, ...rest] = (0, import_core19.outputLines)(block.output).map(sanitize);
4061
+ const [reason, ...rest] = (0, import_core21.outputLines)(block.output).map(sanitize);
3950
4062
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
3951
4063
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
3952
4064
  }
3953
4065
  if (detailAll) {
3954
- const lines2 = (0, import_core19.outputLines)(block.output).map(sanitize);
4066
+ const lines2 = (0, import_core21.outputLines)(block.output).map(sanitize);
3955
4067
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
3956
4068
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
3957
4069
  }
3958
- const { lines, hiddenLineCount } = (0, import_core19.outputPreview)(block.output, PREVIEW_LINES);
4070
+ const { lines, hiddenLineCount } = (0, import_core21.outputPreview)(block.output, PREVIEW_LINES);
3959
4071
  if (lines.length === 0) return [style.grey("(no output)")];
3960
4072
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
3961
4073
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
@@ -3996,7 +4108,8 @@ function subagentLines(block, cols, detailAll) {
3996
4108
  var APPROVAL_KIND = {
3997
4109
  shell_command: "Run a command",
3998
4110
  url_fetch: "Fetch a URL",
3999
- external_path: "Read outside the workspace"
4111
+ external_path: "Read outside the workspace",
4112
+ runner_tool: "Use a tool"
4000
4113
  };
4001
4114
  var DECIDED_BY = {
4002
4115
  "pre-approved": "pre-approved",
@@ -4006,8 +4119,9 @@ var DECIDED_BY = {
4006
4119
  };
4007
4120
  function approvalLine(block, cols) {
4008
4121
  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)}`;
4122
+ 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];
4123
+ const note = block.note ? ` \u2014 \u201C${firstLine(block.note)}\u201D` : "";
4124
+ const line = `${mark} ${status} \xB7 ${APPROVAL_KIND[block.kind]}: ${firstLine(block.subject)}${note}`;
4011
4125
  return `${paint(mark)}${truncate(line, cols).slice(mark.length)}`;
4012
4126
  }
4013
4127
  function planLine(block, cols) {
@@ -4073,38 +4187,8 @@ function tokenLine(blocks, cols) {
4073
4187
  return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
4074
4188
  }
4075
4189
 
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
4190
  // src/tui/state.ts
4107
- var import_core20 = require("@ordewell/core");
4191
+ var import_core22 = require("@ordewell/core");
4108
4192
  var SKILL_IDS = ["tdd", "verify"];
4109
4193
  function noSkills() {
4110
4194
  return { tdd: false, verify: false };
@@ -4134,6 +4218,12 @@ function planRows(state) {
4134
4218
  function selectedPlanRow(state) {
4135
4219
  return planRows(state)[state.selectedTask] ?? null;
4136
4220
  }
4221
+ function waitingApproval(tv) {
4222
+ return tv.view.blocks.find((b) => b.type === "approval" && b.status === "pending" && b.approvalId !== void 0);
4223
+ }
4224
+ function continuesTask(task) {
4225
+ return task?.continuable === true && (task.status === "completed" || task.status === "failed");
4226
+ }
4137
4227
  function findTask(tasks, id) {
4138
4228
  for (const task of tasks) {
4139
4229
  if (task.id === id) return task;
@@ -4145,8 +4235,9 @@ function findTask(tasks, id) {
4145
4235
  function initialState(overrides = {}) {
4146
4236
  return {
4147
4237
  editor: emptyEditor(),
4148
- conversation: import_core20.EMPTY_CONVERSATION,
4149
- turnGate: import_core20.NO_TURN,
4238
+ conversation: import_core22.EMPTY_CONVERSATION,
4239
+ taskView: null,
4240
+ turnGate: import_core22.NO_TURN,
4150
4241
  detailAll: false,
4151
4242
  status: "idle",
4152
4243
  busyLabel: "",
@@ -4171,17 +4262,18 @@ function initialState(overrides = {}) {
4171
4262
  orchestratorModel: "",
4172
4263
  plannerProvider: "",
4173
4264
  plannerEffort: "",
4174
- maxParallel: import_core20.DEFAULT_MAX_PARALLEL,
4265
+ maxParallel: import_core22.DEFAULT_MAX_PARALLEL,
4175
4266
  configuredProviders: [],
4176
4267
  allowlist: {},
4177
4268
  autonomous: true,
4269
+ runnerTransport: "structured",
4178
4270
  mouseCapture: true,
4179
4271
  selection: null,
4180
4272
  workspace: process.cwd(),
4181
4273
  handoff: null,
4182
4274
  overlay: null,
4183
4275
  pendingApprovals: [],
4184
- queuedPrompts: import_core20.EMPTY_HOLD,
4276
+ queuedPrompts: import_core22.EMPTY_HOLD,
4185
4277
  stopArmed: false,
4186
4278
  stopArmToken: 0,
4187
4279
  stopRequested: false,
@@ -4194,19 +4286,53 @@ function initialState(overrides = {}) {
4194
4286
  };
4195
4287
  }
4196
4288
 
4289
+ // src/tui/geometry.ts
4290
+ var PLAN_MIN_COLS = 32;
4291
+ var PLAN_MAX_COLS = 64;
4292
+ var PANE_INDENT = 4;
4293
+ function planPaneWidth(state) {
4294
+ if (state.tasks.length === 0) return 0;
4295
+ const wanted = Math.min(PLAN_MAX_COLS, Math.floor(state.cols * 0.46));
4296
+ return wanted >= PLAN_MIN_COLS && state.cols - wanted > PLAN_MIN_COLS ? wanted : 0;
4297
+ }
4298
+ function chatPaneWidth(state) {
4299
+ const plan = planPaneWidth(state);
4300
+ return plan === 0 ? state.cols : state.cols - plan - 1;
4301
+ }
4302
+ function paneColumns(state, pane) {
4303
+ const plan = planPaneWidth(state);
4304
+ if (plan === 0) return { first: 1, last: state.cols };
4305
+ const chat = chatPaneWidth(state);
4306
+ return pane === "chat" ? { first: 1, last: chat } : { first: chat + 2, last: state.cols };
4307
+ }
4308
+ function paneTextRoom(paneCols) {
4309
+ return Math.max(1, paneCols - PANE_INDENT);
4310
+ }
4311
+ function taskEditorRoom(state) {
4312
+ return paneTextRoom(planPaneWidth(state));
4313
+ }
4314
+ function chatPromptLabel(state) {
4315
+ if (!state.taskView) return "\u276F";
4316
+ const task = findTask(state.tasks, state.taskView.taskId);
4317
+ return `\u2192 ${continuesTask(task) ? "Continue task" : "Task"} ${task?.order ?? "?"}`;
4318
+ }
4319
+ function chatEditorRoomFor(state, active) {
4320
+ return Math.max(1, state.cols - width(chatPromptLabel(state)) - 1 - (active ? 1 : 0));
4321
+ }
4322
+
4197
4323
  // src/tui/layout.ts
4198
- var import_core21 = require("@ordewell/core");
4324
+ var import_core23 = require("@ordewell/core");
4199
4325
  var CHROME_ROWS = 4;
4200
4326
  function chatInputWrap(state) {
4201
4327
  const active = state.focus === "chat" && !state.overlay;
4202
4328
  if (!active) return null;
4203
- return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
4329
+ return wrapLines(state.editor.text, chatEditorRoomFor(state, true));
4204
4330
  }
4205
4331
  function footerHints(state) {
4206
4332
  const selected = selectedPlanRow(state)?.task;
4207
4333
  const markHint = selected?.status === "completed" ? "m undone" : "m done";
4208
4334
  const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
4209
- const planning = plannerInFlight(state);
4335
+ const planning = plannerInFlight(state) && !state.taskView;
4210
4336
  const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
4211
4337
  if (state.focus === "plan") {
4212
4338
  if (state.taskEditor) {
@@ -4234,7 +4360,7 @@ function footerHints(state) {
4234
4360
  "tab chat"
4235
4361
  ];
4236
4362
  }
4237
- const detailHint = (0, import_core21.hasHiddenDetail)(state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4363
+ const detailHint = (0, import_core23.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4238
4364
  return [
4239
4365
  "/help",
4240
4366
  "tab plan",
@@ -4252,6 +4378,43 @@ function queuedBubble(text, cols) {
4252
4378
  const room = Math.max(1, cols - width("\u25C7 \xB7 queued \xB7 esc to unsend"));
4253
4379
  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
4380
  }
4381
+ function taskQueuedRows(state, cols) {
4382
+ const tv = state.taskView;
4383
+ if (!tv) return [];
4384
+ return tv.view.queued.flatMap((message, i) => queuedTaskBubble(message.text, i === tv.queuedIndex, cols));
4385
+ }
4386
+ function queuedTaskBubble(text, selected, cols) {
4387
+ const marker2 = selected ? style.accent("\u276F") : style.grey("\u25C7");
4388
+ const tag = selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4389
+ const room = Math.max(1, cols - width(`\u276F \u25C7 ${tag}`));
4390
+ return wrap(text, room).map((line, i) => i === 0 ? truncate(`${marker2} ${selected ? style.bold(line) : line}${style.grey(tag)}`, cols) : truncate(` ${line}`, cols));
4391
+ }
4392
+ function taskActivity(task, working) {
4393
+ if (task?.awaitingApproval) return approvalLabel(task.awaitingApproval);
4394
+ if (working) return "working";
4395
+ if (task?.status === "awaiting_user") {
4396
+ return task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "waiting for your input";
4397
+ }
4398
+ return task?.status ?? "";
4399
+ }
4400
+ function taskHeaderLines(state, tv, cols) {
4401
+ const task = findTask(state.tasks, tv.taskId);
4402
+ const parts = [
4403
+ `\u2192 Task ${task?.order ?? "?"}`,
4404
+ task?.title ?? "(task removed)",
4405
+ task?.assignedRunner ?? "",
4406
+ taskActivity(task, tv.view.working)
4407
+ ].filter(Boolean);
4408
+ const approval = waitingApproval(tv);
4409
+ 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"];
4410
+ const index = tv.attempts.indexOf(tv.attempt);
4411
+ if (tv.attempts.length > 1) hint.push(`alt\u2190/\u2192 attempt ${index >= 0 ? index + 1 : 1}/${tv.attempts.length}`);
4412
+ hint.push("esc back");
4413
+ return [
4414
+ style.accent(truncate(parts.join(" \xB7 "), cols)),
4415
+ style.grey(truncate(hint.join(" \xB7 "), cols))
4416
+ ];
4417
+ }
4255
4418
  function stopHint(state) {
4256
4419
  return plannerInFlight(state) && state.stopArmed ? "Press Esc again to stop" : "";
4257
4420
  }
@@ -4284,24 +4447,27 @@ function chatBodyLines(blocks, cols, detailAll) {
4284
4447
  return lines;
4285
4448
  }
4286
4449
  function chatLayout(state, rows, cols) {
4287
- const welcome = state.tasks.length === 0;
4288
- const body = chatBodyLines(state.conversation.blocks, cols, state.detailAll);
4450
+ const tv = state.taskView;
4451
+ const blocks = tv ? tv.view.blocks : state.conversation.blocks;
4452
+ const welcome = !tv && state.tasks.length === 0;
4453
+ const body = chatBodyLines(blocks, cols, state.detailAll);
4289
4454
  const transcript = !welcome ? body : body.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
4290
- const bubbles = queuedPromptRows(state, cols);
4455
+ const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
4291
4456
  const lines = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
4292
- const usage = rows > 1 ? tokenLine(state.conversation.blocks, cols) : null;
4457
+ const header = tv ? taskHeaderLines(state, tv, cols) : [];
4458
+ const usage = rows > 1 ? tokenLine(blocks, cols) : null;
4293
4459
  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) };
4460
+ const room = Math.max(0, rows - header.length - footer.length);
4461
+ return { lines, header, footer, anchor: lines.length > room ? "bottom" : "top", maxScroll: Math.max(0, lines.length - room) };
4296
4462
  }
4297
4463
  function chatScrollMax(state) {
4298
4464
  return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
4299
4465
  }
4300
4466
  function plannerSetup(state) {
4301
4467
  const provider = state.plannerProvider;
4302
- const registration = import_core21.ALL_PROVIDERS[provider];
4468
+ const registration = import_core23.ALL_PROVIDERS[provider];
4303
4469
  if (!registration) return { label: "not set", ready: false };
4304
- const runner = (0, import_core21.runnerForProvider)(provider);
4470
+ const runner = (0, import_core23.runnerForProvider)(provider);
4305
4471
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4306
4472
  return { label: registration.label, ready };
4307
4473
  }
@@ -4408,6 +4574,14 @@ var STATUS_ICON = {
4408
4574
  approved: "\xB7",
4409
4575
  pending: "\xB7"
4410
4576
  };
4577
+ var AWAITING_LABEL = {
4578
+ input: "waiting for your input",
4579
+ checkpoint: "checkpoint",
4580
+ conflict: "merge conflict"
4581
+ };
4582
+ function approvalLabel(count) {
4583
+ return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
4584
+ }
4411
4585
  var IDLE_ICON = "~";
4412
4586
  var FIRST_ROW_LINE = 2;
4413
4587
  function planLayout(state, rows, cols) {
@@ -4484,7 +4658,7 @@ function taskLines(state, row, index, cols) {
4484
4658
  const icon = paint(rawIcon);
4485
4659
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
4486
4660
  const caret = selected ? style.cyan("\u276F") : " ";
4487
- const orderLabel = parent ? (0, import_core21.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4661
+ const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4488
4662
  const indent = parent ? " " : "";
4489
4663
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4490
4664
  const titleRoom = Math.max(1, cols - width(head));
@@ -4505,22 +4679,25 @@ function taskLines(state, row, index, cols) {
4505
4679
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4506
4680
  const runner = task.assignedRunner ?? "";
4507
4681
  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 ");
4682
+ const structured = task.transport?.kind === "structured";
4683
+ const waiting = task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "";
4684
+ const activity = task.awaitingApproval ? `${approvalLabel(task.awaitingApproval)} \u2014 t opens it` : idle ? structured ? "quiet" : "quiet \u2014 t opens its terminal" : running ? "working" : waiting;
4685
+ const meta = [activity, runner, structured ? "structured" : "", model].filter(Boolean).join(" \xB7 ");
4510
4686
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4687
+ if (task.transport?.fallback) lines.push(style.yellow(truncate(`${bodyPad}terminal: ${task.transport.fallback}`, cols)));
4511
4688
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4512
4689
  if (task.isolation?.state === "conflict") {
4513
4690
  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)})` : "";
4691
+ const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4515
4692
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4516
4693
  }
4517
4694
  if (task.isolation?.state === "repairing") {
4518
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core21.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4695
+ const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4519
4696
  const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
4520
4697
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4521
4698
  }
4522
4699
  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)));
4700
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core23.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4524
4701
  }
4525
4702
  let editorLine;
4526
4703
  if (expanded) {
@@ -4534,7 +4711,7 @@ function taskLines(state, row, index, cols) {
4534
4711
  }
4535
4712
  }
4536
4713
  if (modeInfo?.autonomous) {
4537
- lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
4714
+ lines.push(...taskText("Autonomy", "Runs without permission prompts (Full auto). Change the level with /auto.", cols, bodyPad));
4538
4715
  }
4539
4716
  const editableSeed = task.prompt ?? task.description ?? task.title;
4540
4717
  if (task.description && task.description !== task.title && task.description !== editableSeed) {
@@ -4596,6 +4773,9 @@ function helpLayout(rows, cols) {
4596
4773
  body.push(
4597
4774
  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
4775
  );
4776
+ body.push(
4777
+ 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")
4778
+ );
4599
4779
  const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
4600
4780
  const room = Math.max(1, rows - 2);
4601
4781
  return { lines, room, maxScroll: Math.max(0, lines.length - room) };
@@ -4605,7 +4785,7 @@ function helpScrollMax(state) {
4605
4785
  }
4606
4786
 
4607
4787
  // src/tui/render.ts
4608
- var import_core22 = require("@ordewell/core");
4788
+ var import_core24 = require("@ordewell/core");
4609
4789
  function render(state) {
4610
4790
  const { rows, cols } = state;
4611
4791
  const rowsForBody = bodyRows(state);
@@ -4614,7 +4794,7 @@ function render(state) {
4614
4794
  renderSkills(state, cols),
4615
4795
  ...body,
4616
4796
  renderStatus(state, cols),
4617
- ...renderInput(state, cols),
4797
+ ...renderInput(state),
4618
4798
  ...renderFooter(state, cols)
4619
4799
  ];
4620
4800
  const frame2 = lines.slice(Math.max(0, lines.length - rows)).map((l) => pad(l, cols));
@@ -4700,8 +4880,9 @@ function renderSkills(state, cols) {
4700
4880
  const badges = SKILL_IDS.map(
4701
4881
  (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
4702
4882
  );
4703
- const auto = state.autonomous ? style.yellow("\u25CF auto") : style.grey("\u25CB auto");
4704
- return truncate([...badges, auto].join(" "), cols);
4883
+ const auto = state.autonomous ? style.yellow("\u25CF Full auto") : style.green("\u25CF Auto");
4884
+ const terminal = state.runnerTransport === "terminal" ? [style.yellow("\u25CF terminal")] : [];
4885
+ return truncate([...badges, auto, ...terminal].join(" "), cols);
4705
4886
  }
4706
4887
  function scrolledBackMark(state) {
4707
4888
  return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
@@ -4746,10 +4927,10 @@ function paintSpans(text, base, spans) {
4746
4927
  }
4747
4928
  return out + text.slice(cursor);
4748
4929
  }
4749
- function renderInput(state, cols) {
4750
- const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
4930
+ function renderInput(state) {
4931
+ const prompt = state.taskView ? style.accent(`${chatPromptLabel(state)} `) : state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
4751
4932
  const lines = chatInputWrap(state);
4752
- const room = chatEditorRoom(cols, lines !== null);
4933
+ const room = chatEditorRoomFor(state, lines !== null);
4753
4934
  const { text, cursor } = state.editor;
4754
4935
  const continuation = " ".repeat(width(prompt));
4755
4936
  const spans = highlightSpans(text);
@@ -4792,16 +4973,18 @@ function renderBody(state, rows, cols) {
4792
4973
  const chatCols = chatPaneWidth(state);
4793
4974
  const chat = renderChat(state, rows, chatCols);
4794
4975
  const plan = renderPlan(state, rows, planCols);
4795
- const divider = `${ERASE_TO_END}${atColumn(chatCols + 1)}${style.grey("\u2502")}`;
4976
+ const border = state.taskView ? style.accent("\u2502") : style.grey("\u2502");
4977
+ const divider = `${ERASE_TO_END}${atColumn(chatCols + 1)}${border}`;
4796
4978
  return Array.from(
4797
4979
  { length: rows },
4798
4980
  (_, i) => `${pad(chat[i] ?? "", chatCols)}${divider}${pad(plan[i] ?? "", planCols)}`
4799
4981
  );
4800
4982
  }
4801
4983
  function renderChat(state, rows, cols) {
4802
- const { lines, footer, anchor, maxScroll } = chatLayout(state, rows, cols);
4984
+ const { lines, header, footer, anchor, maxScroll } = chatLayout(state, rows, cols);
4803
4985
  const back = Math.min(state.scroll, maxScroll);
4804
- return [...fit(lines.slice(0, lines.length - back), rows - footer.length, anchor), ...footer];
4986
+ const body = Math.max(0, rows - header.length - footer.length);
4987
+ return [...header, ...fit(lines.slice(0, lines.length - back), body, anchor), ...footer];
4805
4988
  }
4806
4989
  function renderPlan(state, rows, cols) {
4807
4990
  const layout = planLayout(state, rows, cols);
@@ -4867,7 +5050,7 @@ function renderHandoff(state, overlay, rows, cols) {
4867
5050
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
4868
5051
  }
4869
5052
  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)})`) : "";
5053
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core24.capConflictFiles)(task.repairedFiles)})`) : "";
4871
5054
  return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
4872
5055
  });
4873
5056
  const actions = handoffActions(handoff).map((action, index) => {
@@ -4957,7 +5140,7 @@ function frame(title, body, rows, cols) {
4957
5140
  }
4958
5141
 
4959
5142
  // src/tui/reducer.ts
4960
- var import_core26 = require("@ordewell/core");
5143
+ var import_core30 = require("@ordewell/core");
4961
5144
 
4962
5145
  // src/tui/reducers/shared.ts
4963
5146
  function step(state, effects = []) {
@@ -5006,10 +5189,10 @@ function handleApprovalKey(state, overlay, key) {
5006
5189
  }
5007
5190
 
5008
5191
  // src/tui/reducers/commands.ts
5009
- var import_core25 = require("@ordewell/core");
5192
+ var import_core28 = require("@ordewell/core");
5010
5193
 
5011
5194
  // src/tui/reducers/pickers.ts
5012
- var import_core23 = require("@ordewell/core");
5195
+ var import_core25 = require("@ordewell/core");
5013
5196
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5014
5197
  var DEFAULT_EFFORT = "__runner_default__";
5015
5198
  function picker(title, items, action, extra = {}) {
@@ -5028,11 +5211,11 @@ function modelDetail(m) {
5028
5211
  function providerErrorHint(state) {
5029
5212
  const failed = Object.keys(state.providerErrors ?? {});
5030
5213
  if (failed.length === 0) return void 0;
5031
- const names = failed.map((p) => import_core23.ALL_PROVIDERS[p]?.label ?? p);
5214
+ const names = failed.map((p) => import_core25.ALL_PROVIDERS[p]?.label ?? p);
5032
5215
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5033
5216
  }
5034
5217
  function plannerModelItems(state) {
5035
- const runner = (0, import_core23.runnerForProvider)(state.plannerProvider);
5218
+ const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5036
5219
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5037
5220
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5038
5221
  id: m.id,
@@ -5046,7 +5229,7 @@ function plannerModelItems(state) {
5046
5229
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5047
5230
  }
5048
5231
  function plannerEffortItems(state) {
5049
- const runner = (0, import_core23.runnerForProvider)(state.plannerProvider);
5232
+ const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5050
5233
  if (!runner) {
5051
5234
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5052
5235
  }
@@ -5155,21 +5338,21 @@ function pickerItemsFor(state, action) {
5155
5338
  }
5156
5339
  function plannerItems(state) {
5157
5340
  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);
5341
+ const agents = import_core25.CLI_PROVIDERS.map((id) => {
5342
+ const runner = (0, import_core25.runnerForProvider)(id);
5160
5343
  const ready = installed.has(runner);
5161
5344
  return {
5162
5345
  id,
5163
- label: import_core23.ALL_PROVIDERS[id].label,
5346
+ label: import_core25.ALL_PROVIDERS[id].label,
5164
5347
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5165
5348
  selected: state.plannerProvider === id,
5166
5349
  disabled: !ready
5167
5350
  };
5168
5351
  });
5169
- const vendors = import_core23.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5352
+ const vendors = import_core25.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5170
5353
  id,
5171
- label: import_core23.ALL_PROVIDERS[id].label,
5172
- detail: import_core23.ALL_PROVIDERS[id].apiKeyEnvVar,
5354
+ label: import_core25.ALL_PROVIDERS[id].label,
5355
+ detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
5173
5356
  selected: state.plannerProvider === id
5174
5357
  }));
5175
5358
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -5250,6 +5433,139 @@ function handleMouseSelect(state, key) {
5250
5433
  return step({ ...state, selection: null }, [{ type: "copyText", text: selectedText({ ...state, selection: moved }) }]);
5251
5434
  }
5252
5435
 
5436
+ // src/tui/reducers/taskView.ts
5437
+ var import_core26 = require("@ordewell/core");
5438
+ function openTaskView(state, sessionId, taskId) {
5439
+ const task = findTask(state.tasks, taskId);
5440
+ if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5441
+ const taskView = {
5442
+ taskId,
5443
+ view: import_core26.EMPTY_TASK_LOG,
5444
+ attempts: [],
5445
+ attempt: 0,
5446
+ pending: [],
5447
+ loaded: false,
5448
+ followLatest: true,
5449
+ queuedIndex: 0
5450
+ };
5451
+ return step({ ...state, taskView, focus: "chat", scroll: 0 }, [{ type: "openTaskLog", sessionId, taskId }]);
5452
+ }
5453
+ function openTaskTerminalOrView(state, sessionId, taskId) {
5454
+ const task = findTask(state.tasks, taskId);
5455
+ if (task?.transport?.kind === "structured") return openTaskView(state, sessionId, taskId);
5456
+ return step(state, [{ type: "openTaskTerminal", sessionId, taskId }]);
5457
+ }
5458
+ function continueTaskStep(state, sessionId, taskId, text) {
5459
+ const opened = state.taskView?.taskId === taskId ? step({ ...state, taskView: { ...state.taskView, followLatest: true } }) : openTaskView(state, sessionId, taskId);
5460
+ const watch = state.status !== "executing";
5461
+ return step(opened.state, [...opened.effects, { type: "continueTask", sessionId, taskId, text, ...watch ? { watch: true } : {} }]);
5462
+ }
5463
+ function clampQueueIndex(tv) {
5464
+ const max = Math.max(0, tv.view.queued.length - 1);
5465
+ return tv.queuedIndex <= max ? tv : { ...tv, queuedIndex: max };
5466
+ }
5467
+ function applyLive(tv, attempt, events) {
5468
+ if (attempt <= 0) return tv;
5469
+ let next = tv;
5470
+ if (attempt > next.attempt) {
5471
+ if (!next.followLatest) {
5472
+ return next.attempts.includes(attempt) ? next : { ...next, attempts: [...next.attempts, attempt] };
5473
+ }
5474
+ next = {
5475
+ ...next,
5476
+ view: import_core26.EMPTY_TASK_LOG,
5477
+ attempt,
5478
+ attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5479
+ };
5480
+ }
5481
+ if (attempt < next.attempt) return next;
5482
+ return clampQueueIndex({ ...next, view: events.reduce(import_core26.reduceTaskLog, next.view) });
5483
+ }
5484
+ function taskLogAbandoned(state) {
5485
+ const tv = state.taskView;
5486
+ if (!tv || tv.loaded) return state;
5487
+ let next = { ...tv, pending: [], loaded: true };
5488
+ for (const batch of tv.pending) next = applyLive(next, batch.attempt, batch.events);
5489
+ return { ...state, taskView: clampQueueIndex(next) };
5490
+ }
5491
+ function taskLogArrived(state, action) {
5492
+ const tv = state.taskView;
5493
+ if (!tv || tv.taskId !== action.taskId) return state;
5494
+ if (!tv.loaded) {
5495
+ return { ...state, taskView: { ...tv, pending: [...tv.pending, { attempt: action.attempt, events: action.events }] } };
5496
+ }
5497
+ return { ...state, taskView: applyLive(tv, action.attempt, action.events) };
5498
+ }
5499
+ function announceApprovals(state, action) {
5500
+ if (state.taskView?.taskId === action.taskId) return state;
5501
+ const task = findTask(state.tasks, action.taskId);
5502
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core26.runnerToolSubject)(event.tool, event.args)} \u2014 t on it to answer`) : s, state);
5503
+ return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5504
+ }
5505
+ function taskLogLoaded(state, action) {
5506
+ const tv = state.taskView;
5507
+ if (!tv || tv.taskId !== action.taskId) return state;
5508
+ const loaded = action.attempt >= 1 ? {
5509
+ ...tv,
5510
+ view: (0, import_core26.replayTaskLog)(action.events),
5511
+ attempts: action.attempts ?? tv.attempts,
5512
+ attempt: action.attempt
5513
+ } : { ...tv, attempts: action.attempts ?? tv.attempts };
5514
+ let next = { ...loaded, pending: [], loaded: true };
5515
+ for (const batch of tv.pending) {
5516
+ if (batch.attempt === action.attempt) continue;
5517
+ next = applyLive(next, batch.attempt, batch.events);
5518
+ }
5519
+ return { ...state, taskView: clampQueueIndex(next) };
5520
+ }
5521
+ function answerApproval(state, tv, sessionId, decision) {
5522
+ const approval = waitingApproval(tv);
5523
+ if (!approval?.approvalId) return step(say(state, "system", "Nothing is waiting for approval."));
5524
+ if (decision === "allowForTask" && !approval.allowForTask) {
5525
+ return step(say(state, "system", "The runner offered no grant for the rest of this task \u2014 ctrl-y allows this call."));
5526
+ }
5527
+ const note = decision === "deny" ? state.editor.text.trim() : "";
5528
+ const answer = decision === "deny" ? { decision, ...note ? { note } : {} } : { decision };
5529
+ const next = note ? { ...state, editor: { ...state.editor, text: "", cursor: 0 } } : state;
5530
+ return step(next, [{ type: "answerTaskApproval", sessionId, approvalId: approval.approvalId, answer }]);
5531
+ }
5532
+ function handleTaskViewKey(state, key) {
5533
+ const tv = state.taskView;
5534
+ if (!tv || !state.sessionId) return null;
5535
+ if (key.name === "alt-left" || key.name === "alt-right") {
5536
+ const index = tv.attempts.indexOf(tv.attempt);
5537
+ if (index < 0) return null;
5538
+ const target = key.name === "alt-left" ? index - 1 : index + 1;
5539
+ const attempt = tv.attempts[target];
5540
+ if (attempt === void 0) {
5541
+ return step(say(state, "system", target < 0 ? "This is the first attempt." : "This is the latest attempt."));
5542
+ }
5543
+ return step(
5544
+ { ...state, taskView: { ...tv, followLatest: false } },
5545
+ [{ type: "loadTaskAttempt", sessionId: state.sessionId, taskId: tv.taskId, attempt }]
5546
+ );
5547
+ }
5548
+ if (key.name === "ctrl-r") {
5549
+ const queued = tv.view.queued[tv.queuedIndex];
5550
+ if (!queued) return step(say(state, "system", "No queued message to remove."));
5551
+ return step(state, [{ type: "removeTaskMessage", sessionId: state.sessionId, taskId: tv.taskId, messageId: queued.id }]);
5552
+ }
5553
+ if (key.name === "ctrl-x") {
5554
+ return step(state, [{ type: "interruptTask", sessionId: state.sessionId, taskId: tv.taskId }]);
5555
+ }
5556
+ if (key.name === "ctrl-y") return answerApproval(state, tv, state.sessionId, "allow");
5557
+ if (key.name === "ctrl-t") return answerApproval(state, tv, state.sessionId, "allowForTask");
5558
+ if (key.name === "ctrl-g") return answerApproval(state, tv, state.sessionId, "deny");
5559
+ if (key.name === "ctrl-n" || key.name === "ctrl-p") {
5560
+ const count = tv.view.queued.length;
5561
+ if (count === 0) return null;
5562
+ const delta = key.name === "ctrl-n" ? 1 : -1;
5563
+ const queuedIndex = Math.max(0, Math.min(count - 1, tv.queuedIndex + delta));
5564
+ return step({ ...state, taskView: { ...tv, queuedIndex } });
5565
+ }
5566
+ return null;
5567
+ }
5568
+
5253
5569
  // src/tui/reducers/taskEdits.ts
5254
5570
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
5255
5571
  function openTaskRunnerPicker(state, task) {
@@ -5520,9 +5836,7 @@ function handlePlanKey(state, key) {
5520
5836
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
5521
5837
  }
5522
5838
  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
- }
5839
+ if (key.char === "t") return openTaskTerminalOrView(state, state.sessionId, task.id);
5526
5840
  if (key.char === "x" && task.isolation?.state === "conflict") {
5527
5841
  return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
5528
5842
  }
@@ -5555,9 +5869,9 @@ function commitTaskEdit(state, task, editor) {
5555
5869
  }
5556
5870
 
5557
5871
  // src/tui/reducers/turnQueue.ts
5558
- var import_core24 = require("@ordewell/core");
5872
+ var import_core27 = require("@ordewell/core");
5559
5873
  function drainQueue(state, settled) {
5560
- const next = (0, import_core24.drainNext)(state.queuedPrompts);
5874
+ const next = (0, import_core27.drainNext)(state.queuedPrompts);
5561
5875
  const ended = disarmStop({ ...settled, stopRequested: false });
5562
5876
  if (!next) return step(ended);
5563
5877
  const spoken = say(ended, "user", next.text);
@@ -5576,14 +5890,14 @@ function stopPlanning(state, sessionId) {
5576
5890
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
5577
5891
  }
5578
5892
  function queueToEditor(state) {
5579
- const all = (0, import_core24.unsendAll)(state.queuedPrompts);
5893
+ const all = (0, import_core27.unsendAll)(state.queuedPrompts);
5580
5894
  if (!all) return state;
5581
5895
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
5582
5896
  }
5583
5897
  function unsendLatest(state) {
5584
- const latest = (0, import_core24.unsendLatest)(state.queuedPrompts);
5898
+ const latest = (0, import_core27.unsendLatest)(state.queuedPrompts);
5585
5899
  if (!latest) return step(state);
5586
- const text = (0, import_core24.aheadOfDraft)(latest.text, state.editor.text);
5900
+ const text = (0, import_core27.aheadOfDraft)(latest.text, state.editor.text);
5587
5901
  return step({
5588
5902
  ...state,
5589
5903
  queuedPrompts: latest.rest,
@@ -5600,7 +5914,7 @@ function plannerEscape(state) {
5600
5914
  }
5601
5915
 
5602
5916
  // src/tui/reducers/commands.ts
5603
- var KNOWN_PROVIDERS2 = Object.keys(import_core25.ALL_PROVIDERS);
5917
+ var KNOWN_PROVIDERS2 = Object.keys(import_core28.ALL_PROVIDERS);
5604
5918
  function runCommand(state, { name, args }) {
5605
5919
  if (!findCommand(name)) {
5606
5920
  return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
@@ -5641,6 +5955,8 @@ function runCommand(state, { name, args }) {
5641
5955
  return runners(state, args);
5642
5956
  case "auto":
5643
5957
  return setAutonomous(state, args[0]);
5958
+ case "transport":
5959
+ return setTransport(state, args[0]);
5644
5960
  case "parallel":
5645
5961
  return setMaxParallel(state, args[0]);
5646
5962
  case "mouse":
@@ -5678,11 +5994,13 @@ function runCommand(state, { name, args }) {
5678
5994
  (sessionId, taskId) => step(state, [{ type: "removeTask", sessionId, taskId }])
5679
5995
  );
5680
5996
  case "terminal":
5681
- return taskCommand(
5682
- state,
5683
- args[0],
5684
- (sessionId, taskId) => step(state, [{ type: "openTaskTerminal", sessionId, taskId }])
5685
- );
5997
+ return taskCommand(state, args[0], (sessionId, taskId) => openTaskTerminalOrView(state, sessionId, taskId));
5998
+ case "continue":
5999
+ return taskCommand(state, args[0], (sessionId, taskId) => {
6000
+ const text = args.slice(1).join(" ").trim();
6001
+ if (!text) return fail3(state, "Usage: /continue <id> <message>");
6002
+ return continueTaskStep(state, sessionId, taskId, text);
6003
+ });
5686
6004
  case "task-runner":
5687
6005
  return taskRunnerCommand(state, args);
5688
6006
  case "task-model":
@@ -5749,10 +6067,11 @@ function newSession(state) {
5749
6067
  handoff: null,
5750
6068
  sessionId: null,
5751
6069
  goal: "",
6070
+ taskView: null,
5752
6071
  tasks: [],
5753
6072
  planApproved: false,
5754
- conversation: import_core25.EMPTY_CONVERSATION,
5755
- turnGate: import_core25.NO_TURN,
6073
+ conversation: import_core28.EMPTY_CONVERSATION,
6074
+ turnGate: import_core28.NO_TURN,
5756
6075
  selectedTask: 0,
5757
6076
  expandedTaskId: null,
5758
6077
  taskEditor: null,
@@ -5763,14 +6082,14 @@ function newSession(state) {
5763
6082
  // Prompts belong to the session that raised them; the old planner is gone
5764
6083
  // and its pending requests deny on their own timeout.
5765
6084
  pendingApprovals: [],
5766
- queuedPrompts: import_core25.EMPTY_HOLD,
6085
+ queuedPrompts: import_core28.EMPTY_HOLD,
5767
6086
  stopArmed: false,
5768
6087
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
5769
6088
  }, closeEffects);
5770
6089
  }
5771
6090
  function setModel(state, args) {
5772
6091
  if (args[0] === "set" && args[1]) {
5773
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
6092
+ const runner = (0, import_core28.runnerForProvider)(state.plannerProvider);
5774
6093
  const known = runner ? modelsForRunner(state.models, runner) : [];
5775
6094
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
5776
6095
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -5843,16 +6162,16 @@ function setKey(state, args) {
5843
6162
  }
5844
6163
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
5845
6164
  }
5846
- const items = import_core25.PROVIDER_PRIORITY.map((id) => ({
6165
+ const items = import_core28.PROVIDER_PRIORITY.map((id) => ({
5847
6166
  id,
5848
- label: import_core25.ALL_PROVIDERS[id].label,
5849
- detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
6167
+ label: import_core28.ALL_PROVIDERS[id].label,
6168
+ detail: import_core28.ALL_PROVIDERS[id].apiKeyEnvVar,
5850
6169
  selected: state.configuredProviders.includes(id)
5851
6170
  }));
5852
6171
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
5853
6172
  }
5854
6173
  function keyPrompt(provider) {
5855
- const meta = import_core25.ALL_PROVIDERS[provider];
6174
+ const meta = import_core28.ALL_PROVIDERS[provider];
5856
6175
  return {
5857
6176
  kind: "prompt",
5858
6177
  title: `${meta.label} API key`,
@@ -5929,15 +6248,21 @@ function runners(state, args) {
5929
6248
  }
5930
6249
  function setMaxParallel(state, arg) {
5931
6250
  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);
6251
+ const limit = (0, import_core28.parseMaxParallel)(arg);
5933
6252
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
5934
6253
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
5935
6254
  }
5936
6255
  function setAutonomous(state, arg) {
5937
- const enabled = resolveToggle(arg, state.autonomous);
5938
- if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
6256
+ if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core28.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto auto changes it for new plans.`));
6257
+ const enabled = (0, import_core28.parseAutonomyLevel)(arg);
6258
+ if (enabled === null) return fail3(state, "Usage: /auto [full|auto]");
5939
6259
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
5940
6260
  }
6261
+ function setTransport(state, arg) {
6262
+ const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6263
+ if (!(0, import_core28.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6264
+ return step(state, [{ type: "setTransport", transport: value }]);
6265
+ }
5941
6266
  function setMouseCapture(state, arg) {
5942
6267
  const enabled = resolveToggle(arg, state.mouseCapture);
5943
6268
  if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
@@ -6185,6 +6510,7 @@ function choose(state, picker2, item) {
6185
6510
  }
6186
6511
 
6187
6512
  // src/tui/reducers/incoming.ts
6513
+ var import_core29 = require("@ordewell/core");
6188
6514
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6189
6515
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6190
6516
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6215,6 +6541,7 @@ function toTaskView2(t, index) {
6215
6541
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6216
6542
  type: t.type === "user" ? "user" : "ai",
6217
6543
  status: planLabel(t.status, "pending"),
6544
+ awaitingReason: t.status === "awaiting_user" && (0, import_core29.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6218
6545
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6219
6546
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6220
6547
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6245,6 +6572,7 @@ function applySettings(state, settings) {
6245
6572
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6246
6573
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6247
6574
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6575
+ runnerTransport: (0, import_core29.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6248
6576
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6249
6577
  };
6250
6578
  }
@@ -6279,20 +6607,32 @@ function followSession(state, message) {
6279
6607
  const heard = turnActivity(hear(state, message));
6280
6608
  if (message.type === "approval_request") {
6281
6609
  const { id, kind, subject, scope, detail } = message;
6610
+ if (kind === "runner_tool") return heard;
6282
6611
  return enqueueApproval(heard, { id, kind, subject, scope, ...detail ? { detail } : {} });
6283
6612
  }
6284
6613
  return message.type === "approval_settled" ? dropApproval(heard, message.id) : heard;
6285
6614
  }
6286
6615
 
6287
6616
  // src/tui/reducer.ts
6617
+ function holdReadingPlace(before, after) {
6618
+ if (before.scroll <= 0 || after.scroll !== before.scroll) return after;
6619
+ if (before.taskView?.taskId !== after.taskView?.taskId) return after;
6620
+ const grown = chatScrollMax(after) - chatScrollMax(before);
6621
+ return grown > 0 ? { ...after, scroll: clamp(after.scroll + grown, chatScrollMax(after)) } : after;
6622
+ }
6288
6623
  function reduce(state, action) {
6624
+ const result = reduceAction(state, action);
6625
+ if (action.type !== "sessionMessage" && action.type !== "taskLog") return result;
6626
+ return { ...result, state: holdReadingPlace(state, result.state) };
6627
+ }
6628
+ function reduceAction(state, action) {
6289
6629
  switch (action.type) {
6290
6630
  case "key":
6291
6631
  return handleKey2(state, action.key);
6292
6632
  // Isolation belongs to one session's plan, and task ids repeat across
6293
6633
  // sessions, so nothing of the last one's may carry over into this.
6294
6634
  case "sessionStarted":
6295
- return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
6635
+ return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal, taskView: null }));
6296
6636
  // The daemon registers a session only once planning succeeds; when it does
6297
6637
  // not, holding on to the id would send every next message to a session the
6298
6638
  // server never had.
@@ -6301,14 +6641,15 @@ function reduce(state, action) {
6301
6641
  ...clearIsolation(state),
6302
6642
  sessionId: null,
6303
6643
  goal: "",
6644
+ taskView: null,
6304
6645
  pendingApprovals: [],
6305
- queuedPrompts: import_core26.EMPTY_HOLD,
6646
+ queuedPrompts: import_core30.EMPTY_HOLD,
6306
6647
  stopArmed: false,
6307
6648
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6308
6649
  });
6309
6650
  case "chatRestored":
6310
6651
  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 });
6652
+ return step({ ...state, conversation: (0, import_core30.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6312
6653
  case "planUpdated": {
6313
6654
  if (stale(state, action.sessionId)) return step(state);
6314
6655
  const isolation = isolationOfPlan(action.plan);
@@ -6340,6 +6681,15 @@ function reduce(state, action) {
6340
6681
  case "sessionMessage":
6341
6682
  if (stale(state, action.sessionId)) return step(state);
6342
6683
  return step(followSession(state, action.message));
6684
+ case "taskLog":
6685
+ if (stale(state, action.sessionId)) return step(state);
6686
+ return step(announceApprovals(taskLogArrived(state, action), action));
6687
+ case "taskViewRequested":
6688
+ if (stale(state, action.sessionId)) return step(state);
6689
+ return openTaskView(state, action.sessionId, action.taskId);
6690
+ case "taskLogLoaded":
6691
+ if (stale(state, action.sessionId)) return step(state);
6692
+ return step(taskLogLoaded(state, action));
6343
6693
  // A settled transcript line, not a research step: nothing ever settles a
6344
6694
  // task's start, so as a step it stayed "⋯" forever and was counted into
6345
6695
  // the planner's next "(+N more)".
@@ -6348,12 +6698,12 @@ function reduce(state, action) {
6348
6698
  const existing = findTask(state.tasks, action.taskId);
6349
6699
  if (existing && isTaskRunning(existing)) return step(state);
6350
6700
  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));
6701
+ const spoken = say(state, "system", (0, import_core30.taskStartedNotice)(action.title, action.runner));
6352
6702
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
6353
6703
  }
6354
6704
  case "taskStatus": {
6355
6705
  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);
6706
+ const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0 } : t);
6357
6707
  const status = runStatus(state, tasks);
6358
6708
  return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks) : state.busyLabel });
6359
6709
  }
@@ -6366,9 +6716,13 @@ function reduce(state, action) {
6366
6716
  if (update === void 0) return t;
6367
6717
  const idleSince = update.idleSince ?? null;
6368
6718
  const isolation = update.isolation ?? t.isolation;
6369
- if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
6719
+ const transport = update.transport;
6720
+ const awaitingReason = update.awaitingReason;
6721
+ const continuable = update.continuable === true;
6722
+ const awaitingApproval = update.awaitingApproval;
6723
+ 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
6724
  changed = true;
6371
- return { ...t, status: update.status, idleSince, isolation };
6725
+ return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval };
6372
6726
  }
6373
6727
  return t;
6374
6728
  });
@@ -6443,11 +6797,12 @@ function reduce(state, action) {
6443
6797
  ...clearIsolation(state),
6444
6798
  sessionId: action.sessionId,
6445
6799
  goal: action.goal,
6800
+ taskView: null,
6446
6801
  status: "idle",
6447
6802
  busyLabel: "",
6448
6803
  planApproved: false,
6449
6804
  pendingApprovals: [],
6450
- queuedPrompts: import_core26.EMPTY_HOLD,
6805
+ queuedPrompts: import_core30.EMPTY_HOLD,
6451
6806
  stopArmed: false,
6452
6807
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6453
6808
  });
@@ -6467,6 +6822,7 @@ function reduce(state, action) {
6467
6822
  orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
6468
6823
  }, ["set-planner", "set-task-runner"]));
6469
6824
  case "failed": {
6825
+ state = taskLogAbandoned(state);
6470
6826
  const quiet = plannerInFlight(state) && state.stopRequested;
6471
6827
  const reported = { ...quiet ? state : say(state, "error", action.message), status: "idle", busyLabel: "" };
6472
6828
  if (!plannerInFlight(state)) return step(reported);
@@ -6502,6 +6858,9 @@ function reduce(state, action) {
6502
6858
  return step(disarmStop(state));
6503
6859
  }
6504
6860
  }
6861
+ function sameTransport(a, b) {
6862
+ return a?.kind === b?.kind && a?.fallback === b?.fallback;
6863
+ }
6505
6864
  function runStatus(state, tasks) {
6506
6865
  if (tasks.some(isTaskRunning)) return "executing";
6507
6866
  if (tasks.some((t) => t.status === "awaiting_user")) return "executing";
@@ -6520,6 +6879,10 @@ function handleKey2(state, key) {
6520
6879
  }
6521
6880
  if (key.name === "ctrl-l") return step({ ...state, conversation: wiped(state.conversation), scroll: 0 });
6522
6881
  if (key.name === "ctrl-o" && state.focus === "chat" && !state.overlay) return toggleDetail(state);
6882
+ if (key.name === "escape" && state.taskView && !state.overlay) {
6883
+ if (state.editor.text) return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
6884
+ return step({ ...state, taskView: null, scroll: 0 });
6885
+ }
6523
6886
  if (key.name === "escape") {
6524
6887
  const stopped = plannerEscape(state);
6525
6888
  if (stopped) return stopped;
@@ -6543,12 +6906,16 @@ function handleKey2(state, key) {
6543
6906
  return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
6544
6907
  }
6545
6908
  if (state.focus === "plan") return handlePlanKey(state, key);
6909
+ if (state.taskView) {
6910
+ const handled = handleTaskViewKey(state, key);
6911
+ if (handled) return handled;
6912
+ }
6546
6913
  if (key.name === "enter") return submit(state);
6547
6914
  if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
6548
6915
  const scroll = scrollDelta(key, state);
6549
6916
  if (scroll !== null) return scrollChat(state, scroll);
6550
6917
  const multilineEditor = state.editor.text.includes("\n");
6551
- const room = chatEditorRoom(state.cols, true);
6918
+ const room = chatEditorRoomFor(state, true);
6552
6919
  return step({ ...state, editor: applyKey(state.editor, key, multilineEditor ? room : void 0) });
6553
6920
  }
6554
6921
  function submit(state) {
@@ -6557,8 +6924,14 @@ function submit(state) {
6557
6924
  const cleared = { ...state, editor: commit(state.editor) };
6558
6925
  const command = parseSlash(text);
6559
6926
  if (command && findCommand(command.name)?.source !== "skill") return runCommand(cleared, command);
6927
+ if (state.taskView) {
6928
+ if (!state.sessionId) return step(state);
6929
+ const taskId = state.taskView.taskId;
6930
+ if (continuesTask(findTask(state.tasks, taskId))) return continueTaskStep(cleared, state.sessionId, taskId, text);
6931
+ return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
6932
+ }
6560
6933
  if (plannerInFlight(state)) {
6561
- return step({ ...cleared, queuedPrompts: (0, import_core26.holdPrompt)(state.queuedPrompts, text) });
6934
+ return step({ ...cleared, queuedPrompts: (0, import_core30.holdPrompt)(state.queuedPrompts, text) });
6562
6935
  }
6563
6936
  const spoken = say(cleared, "user", text);
6564
6937
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -6618,10 +6991,10 @@ function createApp(deps) {
6618
6991
 
6619
6992
  // src/tui/effects.ts
6620
6993
  var import_child_process4 = require("child_process");
6621
- var import_core28 = require("@ordewell/core");
6994
+ var import_core32 = require("@ordewell/core");
6622
6995
 
6623
6996
  // src/tui/inbound.ts
6624
- var import_core27 = require("@ordewell/core");
6997
+ var import_core31 = require("@ordewell/core");
6625
6998
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
6626
6999
  "planner_message",
6627
7000
  "planner_turn_started",
@@ -6831,7 +7204,15 @@ function dispatchLifecycle(dispatch, event, sessionId) {
6831
7204
  case "status_update": {
6832
7205
  const updates = {};
6833
7206
  for (const task of event.tasks ?? []) {
6834
- updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
7207
+ updates[String(task.id)] = {
7208
+ status: String(task.status),
7209
+ idleSince: task.idleSince ?? null,
7210
+ isolation: task.isolation,
7211
+ transport: task.transport,
7212
+ awaitingReason: (0, import_core31.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7213
+ ...task.continuable ? { continuable: true } : {},
7214
+ awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0
7215
+ };
6835
7216
  }
6836
7217
  dispatch({ type: "tasksStatus", updates, sessionId });
6837
7218
  return;
@@ -6842,7 +7223,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
6842
7223
  // The extension shows these in its checkpoint panel; here they are
6843
7224
  // transcript lines, which is the TUI's equivalent surface.
6844
7225
  case "checkpoint":
6845
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core27.truncateCheckpointSummary)(event.summary)}` });
7226
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core31.truncateCheckpointSummary)(event.summary)}` });
6846
7227
  return;
6847
7228
  case "review_needed":
6848
7229
  dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
@@ -6875,6 +7256,12 @@ function dispatchLifecycle(dispatch, event, sessionId) {
6875
7256
  case "isolation_handoff":
6876
7257
  dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
6877
7258
  return;
7259
+ // A structured task's log as it happens (ADR-0018, P1): the task view is
7260
+ // its only reader, and the reducer drops it unless that view is open on
7261
+ // this task. The run's watcher itself does nothing with it.
7262
+ case "task_log":
7263
+ dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, sessionId });
7264
+ return;
6878
7265
  // Raw runner chatter. Listed rather than defaulted, so a new lifecycle
6879
7266
  // variant fails to compile here until someone decides what a run's watcher
6880
7267
  // does with it.
@@ -7025,17 +7412,80 @@ async function perform(effect, deps) {
7025
7412
  }
7026
7413
  return;
7027
7414
  }
7415
+ // The card settles when the task log reports the answer, not here.
7416
+ case "answerTaskApproval": {
7417
+ try {
7418
+ await api.respondToApproval(effect.sessionId, effect.approvalId, effect.answer);
7419
+ } catch {
7420
+ dispatch({ type: "failed", message: "Could not deliver the answer \u2014 the task is still waiting for approval." });
7421
+ }
7422
+ return;
7423
+ }
7424
+ // A task with a saved log ran structured, whatever its `transport` says
7425
+ // after a reload; it has no tmux window, so open the log rather than fail.
7028
7426
  case "openTaskTerminal": {
7427
+ const saved = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace).catch(() => []);
7428
+ if (saved.length > 0) {
7429
+ dispatch({ type: "taskViewRequested", sessionId: effect.sessionId, taskId: effect.taskId });
7430
+ return;
7431
+ }
7029
7432
  const result = await deps.openTerminal(effect.sessionId, effect.taskId);
7030
7433
  dispatch(result.ok ? { type: "notice", message: result.message } : { type: "failed", message: result.message });
7031
7434
  return;
7032
7435
  }
7436
+ // The saved log is read off disk (ADR-0018, P1) so the view opens with the
7437
+ // task's history; the live `task_log` stream then catches it up in place.
7438
+ case "openTaskLog": {
7439
+ const attempts = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
7440
+ if (attempts.length === 0) {
7441
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], sessionId: effect.sessionId });
7442
+ return;
7443
+ }
7444
+ const attempt = attempts[attempts.length - 1];
7445
+ const events = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
7446
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, attempt, events, sessionId: effect.sessionId });
7447
+ return;
7448
+ }
7449
+ case "loadTaskAttempt": {
7450
+ const events = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
7451
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempt: effect.attempt, events, sessionId: effect.sessionId });
7452
+ return;
7453
+ }
7454
+ // The daemon queues the message (or delivers it to a waiting task) and
7455
+ // broadcasts the `task_log` event that shows it; nothing to dispatch here.
7456
+ case "sendTaskMessage":
7457
+ await api.sendTaskMessage(effect.sessionId, effect.taskId, effect.text);
7458
+ return;
7459
+ // The daemon starts the new attempt; its `task_log` and status arrive over
7460
+ // the execution stream, which a TUI not already watching a run holds open.
7461
+ case "continueTask": {
7462
+ const { sessionId, taskId, text } = effect;
7463
+ const request = () => api.continueTask(sessionId, taskId, text);
7464
+ await (effect.watch ? withExecutionStream(deps, sessionId, request) : request());
7465
+ await refreshPlan(deps, sessionId);
7466
+ return;
7467
+ }
7468
+ case "removeTaskMessage": {
7469
+ const { removed } = await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
7470
+ if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
7471
+ return;
7472
+ }
7473
+ case "interruptTask":
7474
+ await api.interruptTask(effect.sessionId, effect.taskId);
7475
+ dispatch({ type: "notice", message: "Interrupting the task\u2026" });
7476
+ return;
7033
7477
  case "command": {
7034
7478
  const result = await api.sendCommand(effect.name, { action: effect.action });
7035
7479
  if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7036
7480
  dispatch({ type: "notice", message: `${effect.name} is ${effect.action}.` });
7037
7481
  return;
7038
7482
  }
7483
+ case "setTransport": {
7484
+ const result = await api.sendCommand("transport", { action: effect.transport });
7485
+ if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7486
+ dispatch({ type: "notice", message: `Runner transport is ${effect.transport} \u2014 it applies from the next run.` });
7487
+ return;
7488
+ }
7039
7489
  case "setModel":
7040
7490
  await api.updateSettings({ orchestratorModel: effect.modelId });
7041
7491
  deps.setEnvVar("ORCHESTRATOR_MODEL", effect.modelId);
@@ -7043,7 +7493,7 @@ async function perform(effect, deps) {
7043
7493
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7044
7494
  return;
7045
7495
  case "setPlanner": {
7046
- const meta = import_core28.ALL_PROVIDERS[effect.provider];
7496
+ const meta = import_core32.ALL_PROVIDERS[effect.provider];
7047
7497
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7048
7498
  const env = { AI_PROVIDER: effect.provider };
7049
7499
  const settings = await api.updateSettings({ env });
@@ -7055,7 +7505,7 @@ async function perform(effect, deps) {
7055
7505
  });
7056
7506
  dispatch({
7057
7507
  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)}`
7508
+ message: `${(0, import_core32.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7059
7509
  });
7060
7510
  await loadModels(deps);
7061
7511
  return;
@@ -7067,7 +7517,7 @@ async function perform(effect, deps) {
7067
7517
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7068
7518
  return;
7069
7519
  case "setApiKey": {
7070
- const meta = import_core28.ALL_PROVIDERS[effect.provider];
7520
+ const meta = import_core32.ALL_PROVIDERS[effect.provider];
7071
7521
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7072
7522
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7073
7523
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7105,7 +7555,7 @@ async function perform(effect, deps) {
7105
7555
  return;
7106
7556
  case "setAutonomous":
7107
7557
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
7108
- dispatch({ type: "notice", message: `Autonomous mode ${effect.enabled ? "on" : "off"}.` });
7558
+ dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core32.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7109
7559
  return;
7110
7560
  case "setMouseCapture":
7111
7561
  deps.setMouseCapture(effect.enabled);
@@ -7238,7 +7688,7 @@ function osc52(text) {
7238
7688
  function copySelection(deps, text) {
7239
7689
  if (!text) return;
7240
7690
  const lines = text.split("\n").length;
7241
- const command = (0, import_core28.clipboardCopyCommand)(deps.hasBin);
7691
+ const command = (0, import_core32.clipboardCopyCommand)(deps.hasBin);
7242
7692
  if (command) {
7243
7693
  try {
7244
7694
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -7348,13 +7798,18 @@ var CONTROL2 = {
7348
7798
  "": "ctrl-c",
7349
7799
  "": "ctrl-d",
7350
7800
  "": "ctrl-e",
7801
+ "\x07": "ctrl-g",
7351
7802
  "\v": "ctrl-k",
7352
7803
  "\f": "ctrl-l",
7353
7804
  "": "ctrl-n",
7354
7805
  "": "ctrl-o",
7355
7806
  "": "ctrl-p",
7807
+ "": "ctrl-r",
7808
+ "": "ctrl-t",
7356
7809
  "": "ctrl-u",
7357
- "": "ctrl-w"
7810
+ "": "ctrl-w",
7811
+ "": "ctrl-x",
7812
+ "": "ctrl-y"
7358
7813
  };
7359
7814
  var ESCAPES = {
7360
7815
  "\x1B[A": "up",
@@ -7623,7 +8078,7 @@ async function handleTui(subArgs) {
7623
8078
  }
7624
8079
  const workspace = flag(subArgs, "--workspace") || process.cwd();
7625
8080
  try {
7626
- (0, import_core29.assertWorkspaceExists)(workspace);
8081
+ (0, import_core33.assertWorkspaceExists)(workspace);
7627
8082
  } catch {
7628
8083
  console.error(
7629
8084
  `Workspace "${workspace}" does not exist.
@@ -7632,7 +8087,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7632
8087
  );
7633
8088
  process.exit(1);
7634
8089
  }
7635
- registerSkillCommands((0, import_core29.createSkillsService)(workspace).listSkills());
8090
+ registerSkillCommands((0, import_core33.createSkillsService)(workspace).listSkills());
7636
8091
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
7637
8092
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
7638
8093
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -7649,7 +8104,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7649
8104
  workspace,
7650
8105
  port,
7651
8106
  dispatch: (action) => app.dispatch(action),
7652
- newSessionId: () => (0, import_core29.mintSessionId)(),
8107
+ newSessionId: () => (0, import_core33.mintSessionId)(),
7653
8108
  setEnvVar: (key, value) => {
7654
8109
  writeEnvVar(findEnvFile(), key, value);
7655
8110
  process.env[key] = value;
@@ -7725,6 +8180,7 @@ var COMMANDS = {
7725
8180
  key: handleKey,
7726
8181
  runners: handleRunners,
7727
8182
  auto: handleAuto,
8183
+ transport: handleTransport,
7728
8184
  refresh: handleRefresh,
7729
8185
  setup: handleSetup,
7730
8186
  plugins: handlePlugins,
@@ -7738,6 +8194,7 @@ var COMMANDS = {
7738
8194
  "force-start": handleForceStart,
7739
8195
  "retry": handleRetry,
7740
8196
  "cancel": handleCancel,
8197
+ continue: handleContinue,
7741
8198
  "add-task": handleAddTask,
7742
8199
  "remove-task": handleRemoveTask,
7743
8200
  "task-runner": handleTaskRunner,
@@ -7770,7 +8227,7 @@ function cliVersion() {
7770
8227
  // src/main.ts
7771
8228
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
7772
8229
  async function main() {
7773
- (0, import_core30.migrateOldConfigDir)();
8230
+ (0, import_core34.migrateOldConfigDir)();
7774
8231
  loadEnvFile();
7775
8232
  const argv = process.argv.slice(2);
7776
8233
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");