@ordewell/cli 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +1499 -1148
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -551,6 +551,11 @@ var ApiClient = class {
551
551
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/stop`);
552
552
  return res.data;
553
553
  }
554
+ /** Aborts a planning turn in flight. A harmless no-op when the session isn't planning. */
555
+ async cancelPlanning(sessionId) {
556
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/planning/stop`);
557
+ return res.data;
558
+ }
554
559
  async processQueued(sessionId) {
555
560
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/process-queued`);
556
561
  if (res.status !== 200) {
@@ -1373,14 +1378,18 @@ function fail(...lines) {
1373
1378
  for (const line of lines) console.error(line);
1374
1379
  process.exit(1);
1375
1380
  }
1376
- async function persistEnv(api, env) {
1377
- await api.updateSettings({ env });
1381
+ function writeEnv(env) {
1378
1382
  const envFile = findEnvFile();
1379
1383
  for (const [key, value] of Object.entries(env)) {
1380
1384
  writeEnvVar(envFile, key, value);
1381
1385
  process.env[key] = value;
1382
1386
  }
1383
1387
  }
1388
+ async function persistEnv(api, env) {
1389
+ const settings = await api.updateSettings({ env });
1390
+ writeEnv(env);
1391
+ return settings;
1392
+ }
1384
1393
  function toTaskView(task) {
1385
1394
  return {
1386
1395
  id: task.id,
@@ -2124,6 +2133,24 @@ async function handleSessions(subArgs, injectedApi) {
2124
2133
 
2125
2134
  // src/commands/planner.ts
2126
2135
  var import_core4 = require("@ordewell/core");
2136
+
2137
+ // src/plannerModelSwitch.ts
2138
+ function describePlannerSwitch(settings, provider) {
2139
+ const model = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
2140
+ const effort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
2141
+ if (!model) return { model: "", effort: "", source: "none" };
2142
+ return rememberedModelFor(settings, provider) === model ? { model, effort, source: "remembered" } : { model, effort, source: "catalog-default" };
2143
+ }
2144
+ function rememberedModelFor(settings, provider) {
2145
+ const map = settings.plannerModels;
2146
+ if (!map || typeof map !== "object") return void 0;
2147
+ const entry = map[provider];
2148
+ if (!entry || typeof entry !== "object") return void 0;
2149
+ const { model } = entry;
2150
+ return typeof model === "string" ? model : void 0;
2151
+ }
2152
+
2153
+ // src/commands/planner.ts
2127
2154
  var KNOWN_PROVIDERS = Object.keys(import_core4.ALL_PROVIDERS);
2128
2155
  async function handlePlanner(subArgs, injectedApi) {
2129
2156
  const api = await connect(subArgs, injectedApi);
@@ -2136,31 +2163,28 @@ async function handlePlanner(subArgs, injectedApi) {
2136
2163
  if (!KNOWN_PROVIDERS.includes(provider)) {
2137
2164
  fail(`Unknown planner: ${provider}`, "Run `ordewell planner` with no arguments to see the list.");
2138
2165
  }
2139
- const settings = await api.getSettings();
2140
- const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
2141
- const catalog = await fetchCatalog(api);
2142
- const env = { AI_PROVIDER: provider };
2143
- if (current && !stillServes(catalog, provider, current)) {
2144
- env.ORCHESTRATOR_MODEL = "";
2145
- env.ORDEWELL_PLANNER_EFFORT = "";
2146
- }
2147
- await persistEnv(api, env);
2166
+ const settings = await persistEnv(api, { AI_PROVIDER: provider });
2167
+ const recall2 = describePlannerSwitch(settings, provider);
2168
+ writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
2148
2169
  const meta = import_core4.ALL_PROVIDERS[provider];
2149
- const cleared = env.ORCHESTRATOR_MODEL === "";
2170
+ const catalog = await fetchCatalog(api);
2150
2171
  console.log(
2151
2172
  (0, import_core4.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2152
2173
  );
2153
- if (cleared) {
2154
- console.log(` ${current} is not one of its models, so the planner model was cleared.`);
2155
- console.log(" Pick one with `ordewell model set <id>`, or leave it on the default.");
2156
- }
2174
+ console.log(plannerModelLine(recall2));
2157
2175
  if (!(0, import_core4.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2158
2176
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
2159
2177
  }
2160
2178
  }
2161
- function stillServes(catalog, provider, modelId) {
2162
- const runner = (0, import_core4.runnerForProvider)(provider);
2163
- return runner ? catalog.models.some((m) => m.id === modelId && m.runners?.includes(runner)) : catalog.orchestratorModels.some((m) => m.id === modelId);
2179
+ function plannerModelLine(recall2) {
2180
+ switch (recall2.source) {
2181
+ case "remembered":
2182
+ return ` Restored its planner model: ${recall2.model}.`;
2183
+ case "catalog-default":
2184
+ return ` Using ${recall2.model}, its default model \u2014 pick another with \`ordewell model set <id>\`.`;
2185
+ case "none":
2186
+ return " Pick a model with `ordewell model set <id>`, or leave it on the default.";
2187
+ }
2164
2188
  }
2165
2189
  async function showPlanners(api) {
2166
2190
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
@@ -2655,6 +2679,9 @@ Dependencies \xB7 #${task.order} ${task.title}
2655
2679
  });
2656
2680
  }
2657
2681
 
2682
+ // src/tui/index.ts
2683
+ var import_core10 = require("@ordewell/core");
2684
+
2658
2685
  // src/tui/ansi.ts
2659
2686
  var import_string_width = __toESM(require("string-width"));
2660
2687
  var ANSI = /\x1b\[[0-9;]*m/g;
@@ -2677,6 +2704,9 @@ function truncate(text, max) {
2677
2704
  }
2678
2705
  return out + "\u2026";
2679
2706
  }
2707
+ function stripTabs(text) {
2708
+ return text.replace(/\t/g, " ");
2709
+ }
2680
2710
  function pad(text, target) {
2681
2711
  const current = width(text);
2682
2712
  if (current > target) return truncate(text, target);
@@ -2914,6 +2944,74 @@ function commit(state) {
2914
2944
  return { text: "", cursor: 0, history, historyIndex: history.length, draft: "" };
2915
2945
  }
2916
2946
 
2947
+ // src/tui/slash.ts
2948
+ var SLASH_COMMANDS = [
2949
+ // Planning. There is deliberately no `/plan`: typing the goal *is* the way to
2950
+ // start planning, and an alias for it only shadowed `/planner` on completion.
2951
+ { name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
2952
+ { name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
2953
+ { name: "stop", usage: "/stop", description: "Stop the planner or the running execution, whichever is active", category: "planning" },
2954
+ // Tasks
2955
+ { name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
2956
+ { name: "remove-task", usage: "/remove-task <id>", description: "Remove a task from the plan", category: "tasks" },
2957
+ { name: "complete", usage: "/complete <id>", description: "Mark a task complete", category: "tasks" },
2958
+ { name: "uncomplete", usage: "/uncomplete <id>", description: "Mark a completed task not done", category: "tasks" },
2959
+ { name: "skip", usage: "/skip <id>", description: "Skip a task", category: "tasks" },
2960
+ { name: "retry", usage: "/retry <id>", description: "Re-run a failed task", category: "tasks" },
2961
+ { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
2962
+ { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
2963
+ { name: "terminal", usage: "/terminal <id>", description: "Open a real terminal attached to a task's runner", category: "tasks" },
2964
+ { name: "task-runner", usage: "/task-runner <id> [runner]", description: "Choose or set a task's executor (re-picks its model and mode)", category: "tasks" },
2965
+ { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
2966
+ { name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
2967
+ { name: "task-mode", usage: "/task-mode <id> [mode]", description: "Choose or set a task's runner mode", category: "tasks" },
2968
+ { name: "task-deps", usage: "/task-deps <id>", description: "Edit which earlier tasks a task waits for", category: "tasks" },
2969
+ // Models & providers
2970
+ { name: "planner", usage: "/planner [<provider>]", description: "Choose who plans \u2014 an API provider or a coding agent (no API key)", category: "models" },
2971
+ { name: "model", usage: "/model [set <id>]", description: "Show or set the orchestrator model", category: "models" },
2972
+ { name: "planner-effort", usage: "/planner-effort [<level>]", description: "Thinking effort for a coding-agent planner", category: "models" },
2973
+ { name: "key", usage: "/key [set <provider> <key>]", description: "Configure API provider keys", category: "models" },
2974
+ { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
2975
+ { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
2976
+ { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
2977
+ { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
2978
+ // Skills
2979
+ { name: "grill-me", usage: "/grill-me [on|off]", description: "Grill-Me challenge mode \u2014 the planner interrogates the goal", category: "skills" },
2980
+ { name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
2981
+ { name: "prd", usage: "/prd [on|off]", description: "PRD mode \u2014 write a PRD before the plan", category: "skills" },
2982
+ { name: "review", usage: "/review [on|off]", description: "Review mode \u2014 final opinion-review task with human sign-off", category: "skills" },
2983
+ { name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
2984
+ { name: "research-subagents", usage: "/research-subagents [on|off]", description: "Parallel read-only research subagents during planning", category: "skills" },
2985
+ // Sessions
2986
+ { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
2987
+ { name: "new", usage: "/new", description: "Start a new session", category: "session" },
2988
+ { name: "save", usage: "/save", description: "Save the current session", category: "session" },
2989
+ { name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
2990
+ { name: "delete", usage: "/delete <id>", description: "Delete a saved session", category: "session" },
2991
+ // System
2992
+ { name: "help", usage: "/help", description: "Show available commands", category: "system" },
2993
+ { name: "mouse", usage: "/mouse [on|off]", description: "Wheel scrolling and per-pane drag-select (on), or the terminal's own selection (off)", category: "system" },
2994
+ { name: "quit", usage: "/quit", description: "Exit the TUI", category: "system" }
2995
+ ];
2996
+ var BY_NAME = new Map(SLASH_COMMANDS.map((c) => [c.name, c]));
2997
+ function parseSlash(text) {
2998
+ const trimmed = text.trim();
2999
+ if (!trimmed.startsWith("/")) return null;
3000
+ const parts = trimmed.slice(1).split(/\s+/).filter(Boolean);
3001
+ if (parts.length === 0) return null;
3002
+ return { name: parts[0].toLowerCase(), args: parts.slice(1) };
3003
+ }
3004
+ function findCommand(name) {
3005
+ return BY_NAME.get(name.replace(/^\//, "").toLowerCase());
3006
+ }
3007
+ function completions(text) {
3008
+ if (!text.startsWith("/")) return [];
3009
+ const typed = text.slice(1);
3010
+ if (/\s/.test(typed)) return [];
3011
+ const prefix = typed.toLowerCase();
3012
+ return SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix));
3013
+ }
3014
+
2917
3015
  // src/tui/markdown.ts
2918
3016
  function renderMarkdown(source, cols) {
2919
3017
  const lines = source.replace(/\r/g, "").replace(/^\n+|\n+$/g, "").split("\n");
@@ -2966,16 +3064,16 @@ function tableAt(lines, start) {
2966
3064
  const rows = [];
2967
3065
  let end = start + 1;
2968
3066
  for (let i = start + 2; i < lines.length && lines[i].includes("|"); i += 1) {
2969
- const cells = tableCells(lines[i]);
2970
- if (cells.length !== headers.length) break;
2971
- rows.push(cells);
3067
+ const cells2 = tableCells(lines[i]);
3068
+ if (cells2.length !== headers.length) break;
3069
+ rows.push(cells2);
2972
3070
  end = i;
2973
3071
  }
2974
3072
  return { headers, rows, end };
2975
3073
  }
2976
3074
  function isTableRule(line) {
2977
- const cells = tableCells(line);
2978
- return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
3075
+ const cells2 = tableCells(line);
3076
+ return cells2.length >= 2 && cells2.every((cell) => /^:?-{3,}:?$/.test(cell));
2979
3077
  }
2980
3078
  function tableCells(line) {
2981
3079
  return line.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
@@ -2989,8 +3087,8 @@ function renderTable(headers, rows, cols) {
2989
3087
  );
2990
3088
  const columnWidths = fitTableColumns(natural, available);
2991
3089
  if (columnWidths.some((size) => size < 4)) return renderStackedTable(headers, rows, cols);
2992
- const renderRow = (cells, paint = (text) => text) => {
2993
- const wrapped = cells.map((cell, index) => wrap(inline(cell), columnWidths[index]));
3090
+ const renderRow = (cells2, paint = (text) => text) => {
3091
+ const wrapped = cells2.map((cell, index) => wrap(inline(cell), columnWidths[index]));
2994
3092
  const height = Math.max(...wrapped.map((cell) => cell.length));
2995
3093
  return Array.from(
2996
3094
  { length: height },
@@ -3025,90 +3123,26 @@ function inline(text) {
3025
3123
  return text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+[^)]*)?\)/g, "$1 ($2)").replace(/\*\*([^*]+)\*\*/g, (_, bold) => style.bold(bold)).replace(/__([^_]+)__/g, (_, bold) => style.bold(bold)).replace(/(^|[^\w])\*([^*]+)\*(?![\w*])/g, (_, prefix, italic) => `${prefix}${style.italic(italic)}`).replace(/(^|[^\w])_([^_]+)_(?![\w_])/g, (_, prefix, italic) => `${prefix}${style.italic(italic)}`).replace(/`([^`]+)`/g, (_, code) => style.cyan(code));
3026
3124
  }
3027
3125
 
3028
- // src/tui/slash.ts
3029
- var SLASH_COMMANDS = [
3030
- // Planning. There is deliberately no `/plan`: typing the goal *is* the way to
3031
- // start planning, and an alias for it only shadowed `/planner` on completion.
3032
- { name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
3033
- { name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
3034
- { name: "stop", usage: "/stop", description: "Stop the running execution", category: "planning" },
3035
- // Tasks
3036
- { name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
3037
- { name: "remove-task", usage: "/remove-task <id>", description: "Remove a task from the plan", category: "tasks" },
3038
- { name: "complete", usage: "/complete <id>", description: "Mark a task complete", category: "tasks" },
3039
- { name: "uncomplete", usage: "/uncomplete <id>", description: "Mark a completed task not done", category: "tasks" },
3040
- { name: "skip", usage: "/skip <id>", description: "Skip a task", category: "tasks" },
3041
- { name: "retry", usage: "/retry <id>", description: "Re-run a failed task", category: "tasks" },
3042
- { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
3043
- { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
3044
- { name: "terminal", usage: "/terminal <id>", description: "Open a real terminal attached to a task's runner", category: "tasks" },
3045
- { name: "task-runner", usage: "/task-runner <id> [runner]", description: "Choose or set a task's executor (re-picks its model and mode)", category: "tasks" },
3046
- { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
3047
- { name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
3048
- { name: "task-mode", usage: "/task-mode <id> [mode]", description: "Choose or set a task's runner mode", category: "tasks" },
3049
- { name: "task-deps", usage: "/task-deps <id>", description: "Edit which earlier tasks a task waits for", category: "tasks" },
3050
- // Models & providers
3051
- { name: "planner", usage: "/planner [<provider>]", description: "Choose who plans \u2014 an API provider or a coding agent (no API key)", category: "models" },
3052
- { name: "model", usage: "/model [set <id>]", description: "Show or set the orchestrator model", category: "models" },
3053
- { name: "planner-effort", usage: "/planner-effort [<level>]", description: "Thinking effort for a coding-agent planner", category: "models" },
3054
- { name: "key", usage: "/key [set <provider> <key>]", description: "Configure API provider keys", category: "models" },
3055
- { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3056
- { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3057
- { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3058
- { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3059
- // Skills
3060
- { name: "grill-me", usage: "/grill-me [on|off]", description: "Grill-Me challenge mode \u2014 the planner interrogates the goal", category: "skills" },
3061
- { name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
3062
- { name: "prd", usage: "/prd [on|off]", description: "PRD mode \u2014 write a PRD before the plan", category: "skills" },
3063
- { name: "review", usage: "/review [on|off]", description: "Review mode \u2014 final opinion-review task with human sign-off", category: "skills" },
3064
- { name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
3065
- { name: "research-subagents", usage: "/research-subagents [on|off]", description: "Parallel read-only research subagents during planning", category: "skills" },
3066
- // Sessions
3067
- { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
3068
- { name: "new", usage: "/new", description: "Start a new session", category: "session" },
3069
- { name: "save", usage: "/save", description: "Save the current session", category: "session" },
3070
- { name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
3071
- { name: "delete", usage: "/delete <id>", description: "Delete a saved session", category: "session" },
3072
- // System
3073
- { name: "help", usage: "/help", description: "Show available commands", category: "system" },
3074
- { name: "mouse", usage: "/mouse [on|off]", description: "Wheel scrolling instead of drag-to-select text", category: "system" },
3075
- { name: "quit", usage: "/quit", description: "Exit the TUI", category: "system" }
3076
- ];
3077
- var BY_NAME = new Map(SLASH_COMMANDS.map((c) => [c.name, c]));
3078
- function parseSlash(text) {
3079
- const trimmed = text.trim();
3080
- if (!trimmed.startsWith("/")) return null;
3081
- const parts = trimmed.slice(1).split(/\s+/).filter(Boolean);
3082
- if (parts.length === 0) return null;
3083
- return { name: parts[0].toLowerCase(), args: parts.slice(1) };
3084
- }
3085
- function findCommand(name) {
3086
- return BY_NAME.get(name.replace(/^\//, "").toLowerCase());
3087
- }
3088
- function completions(text) {
3089
- if (!text.startsWith("/")) return [];
3090
- const typed = text.slice(1);
3091
- if (/\s/.test(typed)) return [];
3092
- const prefix = typed.toLowerCase();
3093
- return SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix));
3094
- }
3095
-
3096
- // src/tui/reducer.ts
3097
- var import_core7 = require("@ordewell/core");
3098
- var import_plan_utils3 = require("@ordewell/core/plan-utils");
3099
-
3100
3126
  // src/tui/geometry.ts
3101
3127
  var PLAN_MIN_COLS = 32;
3102
3128
  var PLAN_MAX_COLS = 64;
3103
3129
  var PANE_INDENT = 4;
3104
3130
  var CHAT_PROMPT_COLS = 2;
3105
- var COLLAPSED_TASK_ROWS = 4;
3106
- var EXPANDED_TASK_CHROME_ROWS = 6;
3107
3131
  function planPaneWidth(state) {
3108
3132
  if (state.tasks.length === 0) return 0;
3109
3133
  const wanted = Math.min(PLAN_MAX_COLS, Math.floor(state.cols * 0.46));
3110
3134
  return wanted >= PLAN_MIN_COLS && state.cols - wanted > PLAN_MIN_COLS ? wanted : 0;
3111
3135
  }
3136
+ function chatPaneWidth(state) {
3137
+ const plan = planPaneWidth(state);
3138
+ return plan === 0 ? state.cols : state.cols - plan - 1;
3139
+ }
3140
+ function paneColumns(state, pane) {
3141
+ const plan = planPaneWidth(state);
3142
+ if (plan === 0) return { first: 1, last: state.cols };
3143
+ const chat = chatPaneWidth(state);
3144
+ return pane === "chat" ? { first: 1, last: chat } : { first: chat + 2, last: state.cols };
3145
+ }
3112
3146
  function paneTextRoom(paneCols) {
3113
3147
  return Math.max(1, paneCols - PANE_INDENT);
3114
3148
  }
@@ -3118,19 +3152,19 @@ function taskEditorRoom(state) {
3118
3152
  function chatEditorRoom(cols, active) {
3119
3153
  return Math.max(1, cols - CHAT_PROMPT_COLS - (active ? 1 : 0));
3120
3154
  }
3121
- function planScrollBound(state) {
3122
- let rows = state.tasks.length * COLLAPSED_TASK_ROWS;
3123
- if (state.taskEditor) {
3124
- rows += EXPANDED_TASK_CHROME_ROWS + wrapLines(state.taskEditor.text, taskEditorRoom(state)).length;
3125
- }
3126
- return Math.max(0, rows - 1);
3127
- }
3128
3155
 
3129
3156
  // src/tui/state.ts
3130
3157
  var SKILL_IDS = ["grill-me", "tdd", "prd", "review", "verify", "research-subagents"];
3131
3158
  function noSkills() {
3132
3159
  return { "grill-me": false, tdd: false, prd: false, review: false, verify: false, "research-subagents": false };
3133
3160
  }
3161
+ function visibleItems(picker2) {
3162
+ const filter = picker2.filter.trim().toLowerCase();
3163
+ if (!filter) return picker2.items;
3164
+ return picker2.items.filter(
3165
+ (i) => i.id.toLowerCase().includes(filter) || i.label.toLowerCase().includes(filter) || (i.detail ?? "").toLowerCase().includes(filter)
3166
+ );
3167
+ }
3134
3168
  var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
3135
3169
  var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
3136
3170
  function initialState(overrides = {}) {
@@ -3149,7 +3183,7 @@ function initialState(overrides = {}) {
3149
3183
  expandedTaskId: null,
3150
3184
  taskEditor: null,
3151
3185
  scroll: 0,
3152
- planScroll: 0,
3186
+ planScroll: null,
3153
3187
  skills: noSkills(),
3154
3188
  runners: [],
3155
3189
  sessions: [],
@@ -3163,7 +3197,8 @@ function initialState(overrides = {}) {
3163
3197
  configuredProviders: [],
3164
3198
  allowlist: {},
3165
3199
  autonomous: true,
3166
- mouseCapture: false,
3200
+ mouseCapture: true,
3201
+ selection: null,
3167
3202
  workspace: process.cwd(),
3168
3203
  overlay: null,
3169
3204
  pendingApprovals: [],
@@ -3176,41 +3211,676 @@ function initialState(overrides = {}) {
3176
3211
  };
3177
3212
  }
3178
3213
 
3179
- // src/tui/reducer.ts
3180
- var KNOWN_PROVIDERS2 = Object.keys(import_core7.ALL_PROVIDERS);
3181
- var DEFAULT_EFFORT = "__runner_default__";
3182
- function step(state, effects = []) {
3183
- return { state, effects };
3214
+ // src/tui/layout.ts
3215
+ var import_core7 = require("@ordewell/core");
3216
+ var CHROME_ROWS = 4;
3217
+ function chatInputWrap(state) {
3218
+ const active = state.focus === "chat" && !state.overlay;
3219
+ if (!active) return null;
3220
+ return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
3184
3221
  }
3185
- function say(state, role, content, research) {
3186
- const message = { role, content, timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...research ? { research } : {} };
3187
- return { ...state, messages: [...state.messages, message], scroll: 0 };
3222
+ function footerHints(state) {
3223
+ const markHint = state.tasks[state.selectedTask]?.status === "completed" ? "m undone" : "m done";
3224
+ const planning = state.status === "planning" || state.status === "researching";
3225
+ const escHint = planning ? "esc stop planning" : null;
3226
+ if (state.focus === "plan") {
3227
+ if (state.expandedTaskId) {
3228
+ return ["type to edit prompt", "pgup/pgdn scroll", "alt-enter newline", "enter save", "esc cancel"];
3229
+ }
3230
+ return [
3231
+ ...escHint ? [escHint] : [],
3232
+ "enter expand",
3233
+ "R runner",
3234
+ "o model",
3235
+ "e effort",
3236
+ "M mode",
3237
+ "D deps",
3238
+ "f start",
3239
+ "E run plan",
3240
+ "c cancel",
3241
+ markHint,
3242
+ "s skip",
3243
+ "a add",
3244
+ "d remove",
3245
+ "t terminal",
3246
+ "pgup/pgdn scroll",
3247
+ "tab chat"
3248
+ ];
3249
+ }
3250
+ return [
3251
+ "/help",
3252
+ "tab plan",
3253
+ "alt-enter newline",
3254
+ "pgup/pgdn scroll",
3255
+ ...escHint ? [escHint] : [],
3256
+ "ctrl-c quit"
3257
+ ];
3188
3258
  }
3189
- function restoredMessages(history) {
3190
- return history.map((entry) => ({
3191
- role: entry.kind === "plan_generated" ? "system" : entry.kind === "system" ? "system" : entry.role,
3192
- content: entry.kind === "plan_generated" ? "Plan generated." : entry.content,
3193
- timestamp: entry.timestamp
3194
- }));
3259
+ function packHints(hints, cols) {
3260
+ const lines = [];
3261
+ let current = "";
3262
+ for (const hint of hints) {
3263
+ const next = current ? `${current} \xB7 ${hint}` : hint;
3264
+ if (current && next.length > cols) {
3265
+ lines.push(current);
3266
+ current = hint;
3267
+ } else {
3268
+ current = next;
3269
+ }
3270
+ }
3271
+ lines.push(current);
3272
+ return lines;
3195
3273
  }
3196
- var isPendingResearch = (m) => m.role === "research" && !m.research?.outcome;
3197
- function alreadySpoken(messages, content) {
3198
- const last = findLastIndex(messages, (m) => m.role === "assistant" || m.role === "user");
3199
- return last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
3274
+ function bodyRows(state) {
3275
+ const input = chatInputWrap(state)?.length ?? 1;
3276
+ const footer = packHints(footerHints(state), state.cols).length;
3277
+ return Math.max(0, state.rows - CHROME_ROWS - input - footer + 2);
3200
3278
  }
3201
- function researchLabel(messages, summary) {
3202
- const others = messages.filter(isPendingResearch).length - 1;
3203
- return others > 0 ? `${summary} (+${others} more)` : summary;
3279
+ var ROLE_PREFIX = {
3280
+ user: (t) => `${style.cyan("\u276F")} ${t}`,
3281
+ assistant: (t) => `${style.magenta("\u25C6")} ${t}`,
3282
+ system: (t) => style.grey(`\xB7 ${t}`),
3283
+ error: (t) => `${style.red("\u2717")} ${style.red(t)}`,
3284
+ // Research rows go through `researchRow`, which paints the outcome mark
3285
+ // separately; this entry only exists to keep the role map total.
3286
+ research: (t) => style.grey(` \u21B3 ${t}`)
3287
+ };
3288
+ var OUTCOME_MARK2 = {
3289
+ success: "\u2713",
3290
+ failure: "\u2717",
3291
+ refused: "\u2298",
3292
+ denied: "\u2298",
3293
+ not_executed: "\u2013"
3294
+ };
3295
+ var OUTCOME_PAINT = {
3296
+ success: style.grey,
3297
+ failure: style.red,
3298
+ refused: style.yellow,
3299
+ denied: style.yellow,
3300
+ not_executed: style.grey
3301
+ };
3302
+ var RESEARCH_RESULT_CHARS = 90;
3303
+ function researchLine(message) {
3304
+ const meta = message.research;
3305
+ if (!meta?.outcome) return `\u22EF ${message.content}`;
3306
+ const result = (meta.result ?? "").replace(/\s+/g, " ").trim();
3307
+ const preview = result.length > RESEARCH_RESULT_CHARS ? `${result.slice(0, RESEARCH_RESULT_CHARS)}\u2026` : result;
3308
+ const mark = OUTCOME_MARK2[meta.outcome];
3309
+ return preview ? `${mark} ${message.content} \u2192 ${preview}` : `${mark} ${message.content}`;
3204
3310
  }
3205
- function settleResearchStep(messages, action) {
3206
- const index = findLastIndex(messages, (m) => isPendingResearch(m) && (action.toolCallId ? m.research?.toolCallId === action.toolCallId : m.content === action.summary));
3207
- if (index < 0) return messages;
3208
- const next = [...messages];
3209
- next[index] = {
3210
- ...next[index],
3211
- research: { ...next[index].research, outcome: action.outcome, result: action.result }
3212
- };
3213
- return next;
3311
+ var RESEARCH_INDENT = " \u21B3 ";
3312
+ function researchRow(message, cols) {
3313
+ const text = truncate(researchLine(message), Math.max(1, cols - width(RESEARCH_INDENT)));
3314
+ const paint = OUTCOME_PAINT[message.research?.outcome ?? "success"];
3315
+ const space = text.indexOf(" ");
3316
+ if (space < 0) return style.grey(`${RESEARCH_INDENT}${text}`);
3317
+ return `${style.grey(" \u21B3")} ${paint(text.slice(0, space))} ${style.grey(text.slice(space + 1))}`;
3318
+ }
3319
+ var chatBodyMemo = null;
3320
+ function chatBodyLines(messages, cols) {
3321
+ if (chatBodyMemo && chatBodyMemo.messages === messages && chatBodyMemo.cols === cols) {
3322
+ return chatBodyMemo.lines;
3323
+ }
3324
+ const lines = [];
3325
+ for (const message of messages) {
3326
+ if (message.role === "research") {
3327
+ lines.push(researchRow(message, cols));
3328
+ continue;
3329
+ }
3330
+ const wrapped = message.role === "assistant" ? renderMarkdown(message.content, Math.max(1, cols - 2)) : wrap(message.content, Math.max(1, cols - 2));
3331
+ lines.push(...wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[message.role](line) : ` ${line}`));
3332
+ lines.push("");
3333
+ }
3334
+ chatBodyMemo = { messages, cols, lines };
3335
+ return lines;
3336
+ }
3337
+ function chatLayout(state, rows, cols) {
3338
+ const welcome = state.tasks.length === 0;
3339
+ if (welcome && state.messages.length === 0) {
3340
+ return { lines: welcomeLines(state, cols), anchor: "top", maxScroll: 0 };
3341
+ }
3342
+ const body = chatBodyLines(state.messages, cols);
3343
+ const lines = welcome ? [...welcomeLines(state, cols), "", ...body] : body;
3344
+ return { lines, anchor: "bottom", maxScroll: Math.max(0, lines.length - rows) };
3345
+ }
3346
+ function chatScrollMax(state) {
3347
+ return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
3348
+ }
3349
+ function plannerSetup(state) {
3350
+ const provider = state.plannerProvider;
3351
+ const registration = import_core7.ALL_PROVIDERS[provider];
3352
+ if (!registration) return { label: "not set", ready: false };
3353
+ const runner = (0, import_core7.runnerForProvider)(provider);
3354
+ const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
3355
+ return { label: registration.label, ready };
3356
+ }
3357
+ var SETUP_KEY_COLS = 10;
3358
+ var SETUP_VALUE_COLS = 32;
3359
+ function setupRow(label, value, command, cols) {
3360
+ const gap = " ".repeat(Math.max(2, SETUP_VALUE_COLS - width(value)));
3361
+ const head = ` ${style.grey(pad(label, SETUP_KEY_COLS))}${value}${gap}`;
3362
+ return truncate(`${head}${style.grey(command)}`, cols - 1);
3363
+ }
3364
+ var ICON = `${style.bold("\u226B")}${style.bold(style.cyan("\u25CF"))}`;
3365
+ function boldSans(word) {
3366
+ return [...word].map((ch) => {
3367
+ const code = ch.codePointAt(0);
3368
+ return code >= 97 && code <= 122 ? String.fromCodePoint(120302 + (code - 97)) : ch;
3369
+ }).join("");
3370
+ }
3371
+ var WORDMARK = boldSans("ordewell");
3372
+ var LOCKUP = `${ICON} ${WORDMARK}`;
3373
+ var BANNER_ROWS = [
3374
+ { strokes: "\u281A\u283B\u2836\u28A6\u28E4\u2840", dot: "", word: " \u28B8\u28FF\u2847 \u28B8\u28FF \u28FF\u28FF" },
3375
+ { strokes: " \u2808\u2819\u28B7\u28E4\u2840", dot: "", word: " \u28C0\u28E4\u28E4\u28E4\u28E4\u2840 \u28E4\u28E4\u2880\u28E4\u2844 \u28C0\u28E4\u28E4\u28E4\u28F8\u28FF\u2847 \u28E0\u28E4\u28E4\u28E4\u28C0 \u28A0\u28E4\u2844 \u28E4\u28E4 \u28E0\u28E4 \u28E0\u28E4\u28E4\u28E4\u28C0 \u28B8\u28FF \u28FF\u28FF" },
3376
+ { strokes: "\u28C0\u28C0\u28C0\u28C0\u28C0\u28C0\u28C0\u28C8\u28FB\u28F6\u28E4\u28C0\u28C0", dot: "\u28C0\u28F6\u28FF\u28F7\u2844", word: "\u28F0\u28FF\u280F\u2801\u2808\u28BB\u28FF\u2846 \u28FF\u28FF\u281F\u2809\u2801\u28F0\u28FF\u280F\u2809\u2809\u28BB\u28FF\u2847 \u28FE\u285F\u2801 \u2819\u28FF\u2846 \u28FF\u28E7 \u28B8\u287F\u28FF\u2844 \u28FF\u284F \u28FE\u287F\u2801 \u2819\u28FF\u28C6 \u28B8\u28FF \u28FF\u28FF" },
3377
+ { strokes: "\u2809\u2809\u2809\u2809\u2809\u2809\u2809\u2889\u28FD\u287F\u281B\u280B\u2809", dot: "\u2809\u28BF\u28FF\u287F\u2803", word: "\u28FF\u28FF \u28FF\u2847 \u28FF\u28FF \u28FF\u28FF \u28B8\u28FF\u2847\u28B8\u28FF\u287F\u283F\u283F\u283F\u283F\u281F \u28B8\u28FF\u2840\u28FF\u2807\u28B9\u28C7\u28B8\u28FF\u2801 \u28FF\u287F\u283F\u283F\u283F\u283F\u283F \u28B8\u28FF \u28FF\u28FF" },
3378
+ { strokes: " \u2880\u28E0\u2876\u281B\u2801", dot: "", word: "\u2839\u28FF\u28E6\u28C0\u28C0\u28FC\u28FF\u2803 \u28FF\u28FF \u2838\u28FF\u28E6\u28C0\u28E0\u28FE\u28FF\u2847 \u28BF\u28F7\u28C0\u28C0\u28E0\u28F6\u2806 \u28BF\u28FF\u287F \u2818\u28FF\u28FF\u284F \u28BB\u28F7\u28C4\u28C0\u28E0\u28F6\u2806 \u28B8\u28FF \u28FF\u28FF" },
3379
+ { strokes: "\u28A4\u28E4\u2836\u283E\u281B\u2801", dot: "", word: " \u2808\u2819\u281B\u281B\u2809 \u2809\u2809 \u2808\u2819\u281B\u280B\u2808\u2809\u2801 \u2809\u281B\u281B\u280B\u2801 \u2808\u2809\u2801 \u2809\u2809 \u2809\u281B\u281B\u280B\u2801 \u2809\u2809 \u2809\u2809" }
3380
+ ];
3381
+ var BANNER_ICON_COLS = 18;
3382
+ var BANNER_GAP = 2;
3383
+ var BANNER_COLS = Math.max(
3384
+ ...BANNER_ROWS.map((row) => BANNER_ICON_COLS + BANNER_GAP + width(row.word))
3385
+ );
3386
+ var BANNER_MIN_ROWS = 24;
3387
+ var BANNER_WELL_COL = 30;
3388
+ function colorWell(word) {
3389
+ const cells2 = [...word];
3390
+ return cells2.slice(0, BANNER_WELL_COL).join("") + style.cyan(cells2.slice(BANNER_WELL_COL).join(""));
3391
+ }
3392
+ function bannerLines(rows, cols) {
3393
+ if (cols < BANNER_COLS || rows < BANNER_MIN_ROWS) {
3394
+ const left2 = " ".repeat(Math.max(0, Math.floor((cols - width(LOCKUP)) / 2)));
3395
+ return [left2 + LOCKUP];
3396
+ }
3397
+ const left = " ".repeat(Math.floor((cols - BANNER_COLS) / 2));
3398
+ return BANNER_ROWS.map((row) => {
3399
+ const gap = " ".repeat(BANNER_ICON_COLS + BANNER_GAP - width(row.strokes) - width(row.dot));
3400
+ return left + row.strokes + (row.dot ? style.cyan(row.dot) : "") + gap + colorWell(row.word);
3401
+ });
3402
+ }
3403
+ function welcomeLines(state, cols) {
3404
+ const planner = plannerSetup(state);
3405
+ const enabledRunners = state.runners.filter((r) => r.enabled).map((r) => r.name);
3406
+ const canPlan = planner.ready || state.runners.length > 0 || state.configuredProviders.length > 0;
3407
+ const plannerValue = planner.ready ? [planner.label, state.orchestratorModel || style.yellow("no model")].join(style.grey(" \xB7 ")) : style.yellow(planner.label === "not set" ? "not set" : `${planner.label} (unavailable)`);
3408
+ const lines = [
3409
+ ...bannerLines(state.rows, cols),
3410
+ "",
3411
+ ...wrap(style.bold("Describe a goal to start planning."), Math.max(1, cols - 1)),
3412
+ ...wrap(
3413
+ style.grey("Ordewell researches your codebase, drafts a task plan, then runs each task in a coding agent."),
3414
+ Math.max(1, cols - 1)
3415
+ ),
3416
+ "",
3417
+ style.bold("Setup"),
3418
+ setupRow("Planner", plannerValue, "/planner \xB7 /model", cols),
3419
+ setupRow(
3420
+ "Runners",
3421
+ enabledRunners.length > 0 ? enabledRunners.join(", ") : style.yellow("none enabled"),
3422
+ "/runners",
3423
+ cols
3424
+ ),
3425
+ ""
3426
+ ];
3427
+ if (!canPlan) {
3428
+ lines.push(
3429
+ ...wrap(
3430
+ style.yellow("Ordewell needs one coding agent installed (Claude Code, Codex, OpenCode) \u2014 or an API key from a provider such as OpenRouter, added with /key."),
3431
+ Math.max(1, cols - 1)
3432
+ ),
3433
+ ""
3434
+ );
3435
+ }
3436
+ lines.push(style.grey("/help all commands"), style.grey(state.workspace));
3437
+ return lines;
3438
+ }
3439
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3440
+ var STATUS_MARK = {
3441
+ completed: style.green,
3442
+ running: style.yellow,
3443
+ in_progress: style.yellow,
3444
+ failed: style.red,
3445
+ blocked: style.red,
3446
+ awaiting_user: style.yellow,
3447
+ cancelled: style.grey,
3448
+ skipped: style.grey
3449
+ };
3450
+ var STATUS_ICON = {
3451
+ completed: "\u2713",
3452
+ failed: "\u2717",
3453
+ blocked: "!",
3454
+ awaiting_user: "?",
3455
+ cancelled: "\u2212",
3456
+ skipped: "\u2212",
3457
+ approved: "\xB7",
3458
+ pending: "\xB7"
3459
+ };
3460
+ function planLayout(state, rows, cols) {
3461
+ const done = state.tasks.filter((t) => t.status === "completed").length;
3462
+ const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
3463
+ let selectedLine = 2;
3464
+ let editorLine;
3465
+ state.tasks.forEach((task, i) => {
3466
+ const taskStart = lines.length;
3467
+ if (i === state.selectedTask) selectedLine = taskStart;
3468
+ const renderedTask = taskLines(state, task, i, cols);
3469
+ if (task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
3470
+ editorLine = taskStart + renderedTask.editorLine;
3471
+ }
3472
+ lines.push(...renderedTask.lines);
3473
+ });
3474
+ const maxScroll = Math.max(0, lines.length - rows);
3475
+ const anchorLine = editorLine ?? selectedLine;
3476
+ return { lines, followOffset: Math.min(maxScroll, Math.max(0, anchorLine - rows + 3)), maxScroll };
3477
+ }
3478
+ function planOffset(layout, planScroll) {
3479
+ if (planScroll === null) return layout.followOffset;
3480
+ return Math.max(0, Math.min(layout.maxScroll, planScroll));
3481
+ }
3482
+ function planScrollExtent(state) {
3483
+ const cols = planPaneWidth(state);
3484
+ if (cols === 0) return { followOffset: 0, maxScroll: 0 };
3485
+ const { followOffset, maxScroll } = planLayout(state, bodyRows(state), cols);
3486
+ return { followOffset, maxScroll };
3487
+ }
3488
+ function taskLines(state, task, index, cols) {
3489
+ const selected = state.focus === "plan" && index === state.selectedTask;
3490
+ const expanded = state.expandedTaskId === task.id;
3491
+ const running = isTaskRunning(task);
3492
+ const paint = STATUS_MARK[task.status] ?? ((t) => t);
3493
+ const rawIcon = running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
3494
+ const icon = paint(rawIcon);
3495
+ const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
3496
+ const caret = selected ? style.cyan("\u276F") : " ";
3497
+ const head = `${caret} ${icon} ${String(task.order).padStart(2)} ${kind} `;
3498
+ const titleRoom = Math.max(1, cols - width(head));
3499
+ const wrappedTitle = wrap(task.title, titleRoom);
3500
+ const shownTitle = expanded ? wrappedTitle : wrappedTitle.slice(0, 2);
3501
+ if (!expanded && wrappedTitle.length > shownTitle.length) {
3502
+ const last = shownTitle.length - 1;
3503
+ shownTitle[last] = truncate(`${shownTitle[last]}\u2026`, titleRoom);
3504
+ }
3505
+ const continuation = " ".repeat(width(head));
3506
+ const lines = shownTitle.map((line, lineIndex) => {
3507
+ const prefix = lineIndex === 0 ? head : continuation;
3508
+ return prefix + (selected ? style.bold(line) : line);
3509
+ });
3510
+ const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
3511
+ const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
3512
+ const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}` : "";
3513
+ const runner = task.assignedRunner ?? "";
3514
+ const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
3515
+ if (meta) lines.push(style.grey(truncate(` ${meta}`, cols)));
3516
+ if (effort || mode) lines.push(style.grey(truncate(` ${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
3517
+ let editorLine;
3518
+ if (expanded) {
3519
+ lines.push(style.grey(` ${task.status.replace(/_/g, " ")}`));
3520
+ const editableSeed = task.prompt ?? task.description ?? task.title;
3521
+ if (task.description && task.description !== task.title && task.description !== editableSeed) {
3522
+ lines.push(...taskText("Description", task.description, cols));
3523
+ }
3524
+ if (state.taskEditor) {
3525
+ const room = paneTextRoom(cols);
3526
+ const wrapped = wrapLines(state.taskEditor.text, room);
3527
+ const cp = cursorInLines(wrapped, state.taskEditor.cursor, state.taskEditor.text.length);
3528
+ editorLine = lines.length + 1 + cp.line;
3529
+ lines.push(...taskPromptLines(wrapped, cp));
3530
+ }
3531
+ if (task.dependencies.length > 0) {
3532
+ const orderById = new Map(state.tasks.map((candidate) => [candidate.id, candidate.order]));
3533
+ const dependencies = task.dependencies.map((id) => orderById.has(id) ? `#${orderById.get(id)}` : id).join(", ");
3534
+ lines.push(...taskText("Depends on", dependencies, cols));
3535
+ }
3536
+ lines.push(style.grey(" enter save \xB7 esc cancel"));
3537
+ lines.push(style.grey(truncate(" then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps", cols)));
3538
+ }
3539
+ return { lines, editorLine };
3540
+ }
3541
+ function taskText(label, text, cols) {
3542
+ const room = paneTextRoom(cols);
3543
+ return [
3544
+ style.grey(` ${label}`),
3545
+ ...wrap(text, room).map((line) => ` ${line}`)
3546
+ ];
3547
+ }
3548
+ function taskPromptLines(wrapped, cursor) {
3549
+ return [
3550
+ style.grey(" Prompt"),
3551
+ ...wrapped.map(({ line }, i) => {
3552
+ if (i !== cursor.line) return ` ${line}`;
3553
+ const col = cursor.col;
3554
+ return ` ${line.slice(0, col)}${style.inverse(line.slice(col, col + 1) || " ")}${line.slice(col + 1)}`;
3555
+ })
3556
+ ];
3557
+ }
3558
+ var CATEGORY_TITLE = {
3559
+ planning: "Planning",
3560
+ tasks: "Tasks",
3561
+ models: "Models & providers",
3562
+ skills: "Skills",
3563
+ session: "Sessions",
3564
+ system: "System"
3565
+ };
3566
+ function helpLayout(rows, cols) {
3567
+ const body = [];
3568
+ for (const [category, title] of Object.entries(CATEGORY_TITLE)) {
3569
+ const commands = SLASH_COMMANDS.filter((c) => c.category === category);
3570
+ if (commands.length === 0) continue;
3571
+ body.push(style.bold(title));
3572
+ for (const command of commands) {
3573
+ body.push(` ${pad(command.usage, Math.min(38, cols - 6))} ${style.grey(command.description)}`);
3574
+ }
3575
+ body.push("");
3576
+ }
3577
+ body.push(
3578
+ style.grey("tab switches panes \xB7 \u2191\u2193/pgup/pgdn scroll \xB7 ctrl-l clears \xB7 ctrl-c quits")
3579
+ );
3580
+ const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
3581
+ const room = Math.max(1, rows - 2);
3582
+ return { lines, room, maxScroll: Math.max(0, lines.length - room) };
3583
+ }
3584
+ function helpScrollMax(state) {
3585
+ return helpLayout(bodyRows(state), state.cols).maxScroll;
3586
+ }
3587
+
3588
+ // src/tui/render.ts
3589
+ function render(state) {
3590
+ const { rows, cols } = state;
3591
+ const rowsForBody = bodyRows(state);
3592
+ const body = state.overlay ? renderOverlay(state, rowsForBody, cols) : renderBody(state, rowsForBody, cols);
3593
+ const lines = [
3594
+ renderSkills(state, cols),
3595
+ ...body,
3596
+ renderStatus(state, cols),
3597
+ ...renderInput(state, cols),
3598
+ ...renderFooter(state, cols)
3599
+ ];
3600
+ const frame2 = lines.slice(Math.max(0, lines.length - rows)).map((l) => pad(l, cols));
3601
+ return highlightSelection(frame2, state);
3602
+ }
3603
+ function selectionSpans(state) {
3604
+ const { selection } = state;
3605
+ if (!selection) return [];
3606
+ const { anchor, head } = selection;
3607
+ const forwards = anchor.row < head.row || anchor.row === head.row && anchor.col <= head.col;
3608
+ const [start, end] = forwards ? [anchor, head] : [head, anchor];
3609
+ const { first, last } = paneColumns(state, selection.pane);
3610
+ const spans = [];
3611
+ for (let row = start.row; row <= end.row; row++) {
3612
+ spans.push({
3613
+ row,
3614
+ from: row === start.row ? start.col : first,
3615
+ to: row === end.row ? end.col : last
3616
+ });
3617
+ }
3618
+ return spans;
3619
+ }
3620
+ function cells(line) {
3621
+ const out = [];
3622
+ for (const char of stripAnsi(line)) {
3623
+ out.push(char);
3624
+ for (let i = 1; i < Math.max(1, width(char)); i++) out.push("");
3625
+ }
3626
+ return out;
3627
+ }
3628
+ var INVERSE_ON = "\x1B[7m";
3629
+ var INVERSE_OFF = "\x1B[27m";
3630
+ var SGR_SPLIT = /(\x1b\[[0-9;]*m)/;
3631
+ function invertCells(line, from, to) {
3632
+ let out = "";
3633
+ let col = 1;
3634
+ let inside = false;
3635
+ for (const piece of line.split(SGR_SPLIT)) {
3636
+ if (SGR_SPLIT.test(piece)) {
3637
+ out += piece + (inside ? INVERSE_ON : "");
3638
+ continue;
3639
+ }
3640
+ for (const char of piece) {
3641
+ if (inside && col > to) {
3642
+ out += INVERSE_OFF;
3643
+ inside = false;
3644
+ } else if (!inside && col >= from && col <= to) {
3645
+ out += INVERSE_ON;
3646
+ inside = true;
3647
+ }
3648
+ out += char;
3649
+ col += Math.max(1, width(char));
3650
+ }
3651
+ }
3652
+ return inside ? out + INVERSE_OFF : out;
3653
+ }
3654
+ function highlightSelection(frame2, state) {
3655
+ if (!state.selection || !style.enabled) return frame2;
3656
+ const painted = [...frame2];
3657
+ for (const { row, from, to } of selectionSpans(state)) {
3658
+ const line = painted[row - 1];
3659
+ if (line !== void 0) painted[row - 1] = invertCells(line, from, to);
3660
+ }
3661
+ return painted;
3662
+ }
3663
+ function selectedText(state) {
3664
+ const spans = selectionSpans(state);
3665
+ if (spans.length === 0) return "";
3666
+ const frame2 = render(state);
3667
+ return spans.map(({ row, from, to }) => cells(frame2[row - 1] ?? "").slice(from - 1, to).join("").replace(/\s+$/, "")).join("\n");
3668
+ }
3669
+ function fit(lines, rows, anchor) {
3670
+ if (lines.length >= rows) {
3671
+ return anchor === "bottom" ? lines.slice(lines.length - rows) : lines.slice(0, rows);
3672
+ }
3673
+ const blanks = Array(rows - lines.length).fill("");
3674
+ return anchor === "bottom" ? [...blanks, ...lines] : [...lines, ...blanks];
3675
+ }
3676
+ function renderSkills(state, cols) {
3677
+ const badges = SKILL_IDS.map(
3678
+ (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
3679
+ );
3680
+ const auto = state.autonomous ? style.yellow("\u25CF auto") : style.grey("\u25CB auto");
3681
+ return truncate([...badges, auto].join(" "), cols);
3682
+ }
3683
+ function scrolledBackMark(state) {
3684
+ if (state.focus === "plan") return state.planScroll === null ? "" : "\u2191 scrolled back \xB7 \u2191\u2193 to follow";
3685
+ return state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
3686
+ }
3687
+ function renderStatus(state, cols) {
3688
+ const mark = scrolledBackMark(state);
3689
+ if (state.status === "idle") {
3690
+ const notes = [mark, state.toast].filter(Boolean).join(" \xB7 ");
3691
+ return notes ? truncate(style.grey(notes), cols) : "";
3692
+ }
3693
+ const scrolled = mark ? style.grey(`${mark} \xB7 `) : "";
3694
+ const label = state.busyLabel ? ` ${state.busyLabel}` : "";
3695
+ const verb = state.status === "executing" ? "Executing" : state.status === "researching" ? "Researching" : "Planning";
3696
+ const head = scrolled + style.yellow(`\u25CF ${verb}\u2026${label}`);
3697
+ const thinking = state.thinkingLine.replace(/\s+/g, " ").trim();
3698
+ if (!thinking) return truncate(head, cols);
3699
+ const room = cols - width(head) - 3;
3700
+ if (room < 12) return truncate(head, cols);
3701
+ const tail = thinking.length > room ? `\u2026${thinking.slice(-(room - 1)).trimStart()}` : thinking;
3702
+ return truncate(`${head}${style.grey(` \xB7 ${tail}`)}`, cols);
3703
+ }
3704
+ function renderInput(state, cols) {
3705
+ const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
3706
+ const lines = chatInputWrap(state);
3707
+ const room = chatEditorRoom(cols, lines !== null);
3708
+ const { text, cursor } = state.editor;
3709
+ const continuation = " ".repeat(width(prompt));
3710
+ if (!lines) {
3711
+ const visible = text.replace(/\n/g, "\xB6").replace(/\t/g, " ");
3712
+ return [prompt + visible + suggest(state, room - width(visible))];
3713
+ }
3714
+ const { line: lineIndex, col } = cursorInLines(lines, cursor, text.length);
3715
+ const rawLines = lines.map((l) => l.line);
3716
+ return rawLines.map((raw, i) => {
3717
+ const prefix = i === 0 ? prompt : continuation;
3718
+ const line = raw.replace(/\t/g, " ");
3719
+ const isCursorLine = i === lineIndex;
3720
+ const rendered = isCursorLine ? line.slice(0, col) + style.inverse(line.slice(col, col + 1) || " ") + line.slice(col + 1) : line;
3721
+ const used = width(line) + (isCursorLine && col >= line.length ? 1 : 0);
3722
+ const hint = i === rawLines.length - 1 ? suggest(state, room - used) : "";
3723
+ return prefix + rendered + hint;
3724
+ });
3725
+ }
3726
+ function suggest(state, room) {
3727
+ if (state.focus === "plan" || room <= 2) return "";
3728
+ const matches = completions(state.editor.text);
3729
+ if (matches.length === 0) return "";
3730
+ const names = matches.map((c) => c.name).join(" ");
3731
+ return style.grey(` ${truncate(names, Math.max(0, room - 2))}`);
3732
+ }
3733
+ function renderFooter(state, cols) {
3734
+ return packHints(footerHints(state), cols).map((line) => truncate(style.grey(line), cols));
3735
+ }
3736
+ function renderBody(state, rows, cols) {
3737
+ const planCols = planPaneWidth(state);
3738
+ if (planCols === 0) return renderChat(state, rows, cols);
3739
+ const chatCols = chatPaneWidth(state);
3740
+ const chat = renderChat(state, rows, chatCols);
3741
+ const plan = renderPlan(state, rows, planCols);
3742
+ return Array.from(
3743
+ { length: rows },
3744
+ (_, i) => `${pad(chat[i] ?? "", chatCols)}${style.grey("\u2502")}${pad(plan[i] ?? "", planCols)}`
3745
+ );
3746
+ }
3747
+ function renderChat(state, rows, cols) {
3748
+ const { lines, anchor, maxScroll } = chatLayout(state, rows, cols);
3749
+ const back = Math.min(state.scroll, maxScroll);
3750
+ return fit(lines.slice(0, lines.length - back), rows, anchor);
3751
+ }
3752
+ function renderPlan(state, rows, cols) {
3753
+ const layout = planLayout(state, rows, cols);
3754
+ const offset = planOffset(layout, state.planScroll);
3755
+ return fit([layout.lines[0], ...layout.lines.slice(1 + offset)], rows, "top");
3756
+ }
3757
+ function renderOverlay(state, rows, cols) {
3758
+ const overlay = state.overlay;
3759
+ if (overlay.kind === "help") return renderHelp(overlay.scroll ?? 0, rows, cols);
3760
+ if (overlay.kind === "approval") {
3761
+ const { request } = overlay;
3762
+ const headline = request.kind === "shell_command" ? "The planner wants to run a command" : request.kind === "url_fetch" ? "The planner wants to fetch a URL" : "The planner wants to read a path outside the workspace";
3763
+ const queued = state.pendingApprovals.length;
3764
+ return frame(
3765
+ "Approval needed",
3766
+ [
3767
+ headline,
3768
+ "",
3769
+ ` ${style.yellow(request.subject)}`,
3770
+ "",
3771
+ // Disambiguates the `external_path` case: an auto-tier command like
3772
+ // `cat` touching an outside path reads identically to a plain
3773
+ // `read_file` without it.
3774
+ ...request.detail ? [style.grey(request.detail), ""] : [],
3775
+ // The grant is wider than the request, so name it rather than letting
3776
+ // the user discover it when the next call silently does not prompt.
3777
+ style.grey(`Approving also allows ${request.scope} for the rest of this session.`),
3778
+ ...queued > 0 ? [style.grey(`${queued} more waiting.`)] : [],
3779
+ "",
3780
+ // ESC is not "deny" while a turn is in flight — it kills the turn and
3781
+ // takes this prompt with it. Offering it as the deny key there would
3782
+ // teach the wrong thing about the most destructive key on the sheet.
3783
+ style.grey(state.status === "planning" || state.status === "researching" ? "y or enter allows \xB7 n denies \xB7 esc stops planning" : "y or enter allows \xB7 n or esc denies")
3784
+ ],
3785
+ rows,
3786
+ cols
3787
+ );
3788
+ }
3789
+ if (overlay.kind === "confirm") {
3790
+ return frame(
3791
+ overlay.title,
3792
+ [overlay.message, "", style.grey("enter confirms \xB7 esc cancels")],
3793
+ rows,
3794
+ cols
3795
+ );
3796
+ }
3797
+ if (overlay.kind === "prompt") {
3798
+ const secret = overlay.action.kind === "api-key";
3799
+ const shown = secret ? "\u2022".repeat(overlay.value.length) : overlay.value;
3800
+ return frame(
3801
+ overlay.title,
3802
+ [overlay.hint ? style.grey(overlay.hint) : "", "", `${style.cyan("\u276F")} ${shown}`, "", style.grey("enter confirms \xB7 esc cancels")],
3803
+ rows,
3804
+ cols
3805
+ );
3806
+ }
3807
+ return renderPicker(overlay.picker, rows, cols);
3808
+ }
3809
+ function renderPicker(picker2, rows, cols) {
3810
+ const items = visibleItems(picker2);
3811
+ const body = [];
3812
+ if (picker2.hint) body.push(style.grey(picker2.hint));
3813
+ body.push(`${style.grey("filter:")} ${picker2.filter}${style.grey("\u258F")}`, "");
3814
+ if (items.length === 0) {
3815
+ body.push(style.grey(picker2.filter ? "Nothing matches that filter." : "Nothing to show yet\u2026"));
3816
+ }
3817
+ const room = Math.max(1, rows - body.length - 3);
3818
+ const start = Math.max(0, Math.min(picker2.index - Math.floor(room / 2), items.length - room));
3819
+ for (const [offset, item] of items.slice(start, start + room).entries()) {
3820
+ const active = start + offset === picker2.index;
3821
+ const mark = picker2.multi ? picker2.chosen.includes(item.id) ? style.green("[\u2713]") : style.grey("[ ]") : item.selected ? style.green("\u25CF") : " ";
3822
+ const caret = active ? style.cyan("\u276F") : " ";
3823
+ const label = truncate(item.label, Math.max(1, cols - 12));
3824
+ const detail = item.detail ? style.grey(` \u2014 ${item.detail}`) : "";
3825
+ const line = item.disabled ? `${caret} ${mark} ${style.grey(label)}${detail}` : `${caret} ${mark} ${active ? style.bold(label) : label}${detail}`;
3826
+ body.push(truncate(line, cols - 2));
3827
+ }
3828
+ body.push("", style.grey(picker2.multi ? "space toggles \xB7 enter confirms \xB7 esc cancels \xB7 type to filter" : "enter selects \xB7 esc cancels \xB7 type to filter"));
3829
+ return frame(picker2.title, body, rows, cols);
3830
+ }
3831
+ function renderHelp(scroll, rows, cols) {
3832
+ const { lines, room, maxScroll } = helpLayout(rows, cols);
3833
+ const offset = Math.min(scroll, maxScroll);
3834
+ const footer = style.grey(offset < maxScroll ? "\u2191\u2193 scroll \xB7 any other key closes" : "any key closes");
3835
+ return frame("Commands", [...lines.slice(offset, offset + room), footer], rows, cols);
3836
+ }
3837
+ function frame(title, body, rows, cols) {
3838
+ const inner = Math.max(1, cols - 2);
3839
+ const lines = [
3840
+ style.grey(`\u250C\u2500 ${style.bold(title)} ${"\u2500".repeat(Math.max(0, inner - width(title) - 3))}`),
3841
+ ...body.flatMap((line) => wrap(line, inner)).map((line) => ` ${truncate(line, inner)}`)
3842
+ ];
3843
+ return fit(lines, rows, "top");
3844
+ }
3845
+
3846
+ // src/tui/reducer.ts
3847
+ var import_core8 = require("@ordewell/core");
3848
+ var import_plan_utils3 = require("@ordewell/core/plan-utils");
3849
+ var KNOWN_PROVIDERS2 = Object.keys(import_core8.ALL_PROVIDERS);
3850
+ var DEFAULT_EFFORT = "__runner_default__";
3851
+ function step(state, effects = []) {
3852
+ return { state, effects };
3853
+ }
3854
+ function say(state, role, content, research) {
3855
+ const message = { role, content: stripTabs(content), timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...research ? { research } : {} };
3856
+ return { ...state, messages: [...state.messages, message], scroll: 0 };
3857
+ }
3858
+ function restoredMessages(history) {
3859
+ return history.map((entry) => ({
3860
+ role: entry.kind === "plan_generated" ? "system" : entry.kind === "system" ? "system" : entry.role,
3861
+ content: stripTabs(entry.kind === "plan_generated" ? "Plan generated." : entry.content),
3862
+ timestamp: entry.timestamp
3863
+ }));
3864
+ }
3865
+ var isPendingResearch = (m) => m.role === "research" && !m.research?.outcome;
3866
+ function alreadySpoken(messages, content) {
3867
+ const last = findLastIndex(messages, (m) => m.role === "assistant" || m.role === "user");
3868
+ return last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
3869
+ }
3870
+ function researchLabel(messages, summary) {
3871
+ const others = messages.filter(isPendingResearch).length - 1;
3872
+ return others > 0 ? `${summary} (+${others} more)` : summary;
3873
+ }
3874
+ function settleResearchStep(messages, action) {
3875
+ const summary = stripTabs(action.summary);
3876
+ const index = findLastIndex(messages, (m) => isPendingResearch(m) && (action.toolCallId ? m.research?.toolCallId === action.toolCallId : m.content === summary));
3877
+ if (index < 0) return messages;
3878
+ const next = [...messages];
3879
+ next[index] = {
3880
+ ...next[index],
3881
+ research: { ...next[index].research, outcome: action.outcome, result: stripTabs(action.result) }
3882
+ };
3883
+ return next;
3214
3884
  }
3215
3885
  function findLastIndex(items, match) {
3216
3886
  for (let i = items.length - 1; i >= 0; i--) {
@@ -3266,21 +3936,26 @@ function reduce(state, action) {
3266
3936
  case "plannerMessage": {
3267
3937
  if (stale(state, action.sessionId)) return step(state);
3268
3938
  const settled = { ...state, status: "idle", busyLabel: "", thinkingLine: "" };
3269
- if (alreadySpoken(state.messages, action.content)) return step(settled);
3270
- return step(say(settled, "assistant", action.content));
3939
+ const content = stripTabs(action.content);
3940
+ if (alreadySpoken(state.messages, content)) return step(settled);
3941
+ return step(say(settled, "assistant", content));
3271
3942
  }
3272
3943
  case "researchStep": {
3273
3944
  if (stale(state, action.sessionId)) return step(state);
3274
- const spoken = say(state, "research", action.summary, { toolCallId: action.toolCallId });
3275
- return step({ ...spoken, busyLabel: researchLabel(spoken.messages, action.summary) });
3945
+ const summary = stripTabs(action.summary);
3946
+ const spoken = say(state, "research", summary, { toolCallId: action.toolCallId });
3947
+ const status = state.status === "planning" ? "researching" : state.status;
3948
+ return step({ ...spoken, status, busyLabel: researchLabel(spoken.messages, summary) });
3276
3949
  }
3277
3950
  case "researchStepDone": {
3278
3951
  if (stale(state, action.sessionId)) return step(state);
3279
3952
  const messages = settleResearchStep(state.messages, action);
3280
3953
  const pending = messages.filter(isPendingResearch);
3954
+ const status = pending.length === 0 && state.status === "researching" ? "planning" : state.status;
3281
3955
  return step({
3282
3956
  ...state,
3283
3957
  messages,
3958
+ status,
3284
3959
  busyLabel: pending.length > 0 ? researchLabel(messages, pending[pending.length - 1].content) : ""
3285
3960
  });
3286
3961
  }
@@ -3361,8 +4036,14 @@ function reduce(state, action) {
3361
4036
  return step({ ...say(state, "error", action.message), status: "idle", busyLabel: "", thinkingLine: "" });
3362
4037
  case "notice":
3363
4038
  return step(say(state, "system", action.message));
3364
- case "resize":
3365
- return step({ ...state, rows: action.rows, cols: action.cols });
4039
+ case "resize": {
4040
+ const resized = { ...state, rows: action.rows, cols: action.cols, selection: null };
4041
+ return step({
4042
+ ...resized,
4043
+ scroll: clamp(resized.scroll, chatScrollMax(resized)),
4044
+ planScroll: resized.planScroll === null ? null : clamp(resized.planScroll, planScrollExtent(resized).maxScroll)
4045
+ });
4046
+ }
3366
4047
  // Whether there is anything to animate is the app loop's call (it owns the
3367
4048
  // timer); a tick that arrives simply advances the frame.
3368
4049
  case "spinnerTick":
@@ -3434,11 +4115,11 @@ function modelDetail(m) {
3434
4115
  function providerErrorHint(state) {
3435
4116
  const failed = Object.keys(state.providerErrors ?? {});
3436
4117
  if (failed.length === 0) return void 0;
3437
- const names = failed.map((p) => import_core7.ALL_PROVIDERS[p]?.label ?? p);
4118
+ const names = failed.map((p) => import_core8.ALL_PROVIDERS[p]?.label ?? p);
3438
4119
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
3439
4120
  }
3440
4121
  function plannerModelItems(state) {
3441
- const runner = (0, import_core7.runnerForProvider)(state.plannerProvider);
4122
+ const runner = (0, import_core8.runnerForProvider)(state.plannerProvider);
3442
4123
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
3443
4124
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
3444
4125
  id: m.id,
@@ -3449,7 +4130,7 @@ function plannerModelItems(state) {
3449
4130
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
3450
4131
  }
3451
4132
  function plannerEffortItems(state) {
3452
- const runner = (0, import_core7.runnerForProvider)(state.plannerProvider);
4133
+ const runner = (0, import_core8.runnerForProvider)(state.plannerProvider);
3453
4134
  if (!runner) {
3454
4135
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
3455
4136
  }
@@ -3548,13 +4229,80 @@ function pickerItemsFor(state, action) {
3548
4229
  }
3549
4230
  return [];
3550
4231
  }
4232
+ var WHEEL_NOTCH = 3;
4233
+ function pageNotch(viewport) {
4234
+ return Math.max(1, viewport - 1);
4235
+ }
4236
+ function scrollDelta(key, state) {
4237
+ if (key.name === "scrollup") return WHEEL_NOTCH;
4238
+ if (key.name === "scrolldown") return -WHEEL_NOTCH;
4239
+ if (key.name === "pageup") return pageNotch(bodyRows(state));
4240
+ if (key.name === "pagedown") return -pageNotch(bodyRows(state));
4241
+ return null;
4242
+ }
4243
+ var isWheel = (key) => key.name === "scrollup" || key.name === "scrolldown";
4244
+ function pointerPane(state, key) {
4245
+ if (key.col === void 0 || planPaneWidth(state) === 0) return state.focus;
4246
+ return key.col <= chatPaneWidth(state) ? "chat" : "plan";
4247
+ }
4248
+ function scrollPointed(state, key) {
4249
+ const delta = key.name === "scrollup" ? WHEEL_NOTCH : -WHEEL_NOTCH;
4250
+ return pointerPane(state, key) === "plan" ? scrollPlan(state, delta) : scrollChat(state, delta);
4251
+ }
4252
+ var isMouseSelect = (key) => key.name === "mousedown" || key.name === "mousedrag" || key.name === "mouseup";
4253
+ function selectionCell(state, pane, key) {
4254
+ const { first, last } = paneColumns(state, pane);
4255
+ return {
4256
+ col: clampRange(key.col ?? first, first, last),
4257
+ row: clampRange(key.row ?? 1, 1, state.rows)
4258
+ };
4259
+ }
4260
+ function clampRange(value, low, high) {
4261
+ return Math.max(low, Math.min(high, value));
4262
+ }
4263
+ var isClick = (selection) => selection.anchor.col === selection.head.col && selection.anchor.row === selection.head.row;
4264
+ function handleMouseSelect(state, key) {
4265
+ if (key.name === "mousedown") {
4266
+ const pane = pointerPane(state, key);
4267
+ const cell = selectionCell(state, pane, key);
4268
+ return step({ ...state, selection: { anchor: cell, head: cell, pane } });
4269
+ }
4270
+ const { selection } = state;
4271
+ if (!selection) return step(state);
4272
+ const moved = { ...selection, head: selectionCell(state, selection.pane, key) };
4273
+ if (key.name === "mousedrag") return step({ ...state, selection: moved });
4274
+ if (isClick(moved)) return step({ ...state, selection: null });
4275
+ return step({ ...state, selection: null }, [{ type: "copyText", text: selectedText({ ...state, selection: moved }) }]);
4276
+ }
4277
+ function clamp(value, max) {
4278
+ return Math.max(0, Math.min(max, value));
4279
+ }
4280
+ function scrollChat(state, delta) {
4281
+ return step({ ...state, scroll: clamp(state.scroll + delta, chatScrollMax(state)) });
4282
+ }
4283
+ function scrollPlan(state, delta) {
4284
+ const { followOffset, maxScroll } = planScrollExtent(state);
4285
+ const from = state.planScroll ?? followOffset;
4286
+ return step({ ...state, planScroll: clamp(from - delta, maxScroll) });
4287
+ }
4288
+ var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
4289
+ function stopPlanning(state, sessionId) {
4290
+ const dismissed = state.overlay?.kind === "approval" ? { ...state, overlay: null, pendingApprovals: [] } : state;
4291
+ return step(dismissed, [{ type: "cancelPlanning", sessionId }]);
4292
+ }
3551
4293
  function handleKey2(state, key) {
3552
4294
  if (key.name === "ctrl-c") return interrupt(state);
3553
4295
  if (key.name === "ctrl-d" && !state.editor.text && !state.overlay) {
3554
4296
  return step({ ...state, exiting: true }, [{ type: "exit" }]);
3555
4297
  }
3556
4298
  if (key.name === "ctrl-l") return step({ ...state, messages: [] });
4299
+ if (key.name === "escape" && plannerInFlight(state) && state.sessionId && (!state.overlay || state.overlay.kind === "approval")) {
4300
+ return stopPlanning(state, state.sessionId);
4301
+ }
3557
4302
  if (state.overlay) return handleOverlayKey(state, state.overlay, key);
4303
+ if (isWheel(key)) return scrollPointed(state, key);
4304
+ if (isMouseSelect(key)) return handleMouseSelect(state, key);
4305
+ if (key.name === "wheelignored") return step(state);
3558
4306
  if (key.name === "tab" && state.focus === "chat") {
3559
4307
  const matches = completions(state.editor.text);
3560
4308
  if (matches.length > 0) {
@@ -3566,18 +4314,9 @@ function handleKey2(state, key) {
3566
4314
  if (state.focus === "plan") return handlePlanKey(state, key);
3567
4315
  if (key.name === "enter") return submit(state);
3568
4316
  if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
3569
- if (key.name === "pageup" || key.name === "pagedown") {
3570
- const page = Math.max(1, state.rows - 6);
3571
- const scroll = key.name === "pageup" ? state.scroll + page : Math.max(0, state.scroll - page);
3572
- return step({ ...state, scroll });
3573
- }
4317
+ const scroll = scrollDelta(key, state);
4318
+ if (scroll !== null) return scrollChat(state, scroll);
3574
4319
  const multilineEditor = state.editor.text.includes("\n");
3575
- if (key.name === "scrollup" || key.name === "scrolldown") {
3576
- const notch = 3;
3577
- const backward = key.name === "scrollup";
3578
- const scroll = backward ? state.scroll + notch : Math.max(0, state.scroll - notch);
3579
- return step({ ...state, scroll });
3580
- }
3581
4320
  const room = chatEditorRoom(state.cols, true);
3582
4321
  return step({ ...state, editor: applyKey(state.editor, key, multilineEditor ? room : void 0) });
3583
4322
  }
@@ -3604,24 +4343,18 @@ function handlePlanKey(state, key) {
3604
4343
  if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
3605
4344
  if (key.name === "escape") return step({ ...state, focus: "chat" });
3606
4345
  if (key.name === "up") {
3607
- return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1), expandedTaskId: null, planScroll: 0 });
4346
+ return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1), expandedTaskId: null, planScroll: null });
3608
4347
  }
3609
4348
  if (key.name === "down") {
3610
4349
  return step({
3611
4350
  ...state,
3612
4351
  selectedTask: Math.min(state.tasks.length - 1, state.selectedTask + 1),
3613
4352
  expandedTaskId: null,
3614
- planScroll: 0
4353
+ planScroll: null
3615
4354
  });
3616
4355
  }
3617
- if (key.name === "scrollup" || key.name === "pageup") {
3618
- const notch = key.name === "pageup" ? 10 : 3;
3619
- return step({ ...state, planScroll: Math.max(0, state.planScroll - notch) });
3620
- }
3621
- if (key.name === "scrolldown" || key.name === "pagedown") {
3622
- const notch = key.name === "pagedown" ? 10 : 3;
3623
- return step({ ...state, planScroll: Math.min(planScrollBound(state), state.planScroll + notch) });
3624
- }
4356
+ const scroll = scrollDelta(key, state);
4357
+ if (scroll !== null) return scrollPlan(state, scroll);
3625
4358
  const task = state.tasks[state.selectedTask];
3626
4359
  if (!task) return step(state);
3627
4360
  if (key.name === "enter" || key.name === "right") {
@@ -3653,14 +4386,8 @@ function handlePlanKey(state, key) {
3653
4386
  function handleTaskEditKey(state, task, editor, key) {
3654
4387
  if (key.name === "enter") return commitTaskEdit(state, task, editor);
3655
4388
  if (key.name === "escape") return step({ ...state, expandedTaskId: null, taskEditor: null });
3656
- if (key.name === "scrollup" || key.name === "pageup") {
3657
- const notch = key.name === "pageup" ? 10 : 3;
3658
- return step({ ...state, planScroll: Math.max(0, state.planScroll - notch) });
3659
- }
3660
- if (key.name === "scrolldown" || key.name === "pagedown") {
3661
- const notch = key.name === "pagedown" ? 10 : 3;
3662
- return step({ ...state, planScroll: Math.min(planScrollBound(state), state.planScroll + notch) });
3663
- }
4389
+ const scroll = scrollDelta(key, state);
4390
+ if (scroll !== null) return scrollPlan(state, scroll);
3664
4391
  return step({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) });
3665
4392
  }
3666
4393
  function commitTaskEdit(state, task, editor) {
@@ -3705,6 +4432,8 @@ function handleApprovalKey(state, overlay, key) {
3705
4432
  }
3706
4433
  function handleOverlayKey(state, overlay, key) {
3707
4434
  if (overlay.kind === "help") return handleHelpKey(state, overlay.scroll ?? 0, key);
4435
+ if (key.name === "wheelignored") return step(state);
4436
+ if (isWheel(key) && overlay.kind !== "picker") return scrollPointed(state, key);
3708
4437
  if (overlay.kind === "approval") return handleApprovalKey(state, overlay, key);
3709
4438
  if (overlay.kind === "confirm") return handleConfirmKey(state, overlay, key);
3710
4439
  if (key.name === "escape") return step({ ...state, overlay: null });
@@ -3723,17 +4452,18 @@ function handleConfirmKey(state, overlay, key) {
3723
4452
  return step(closed);
3724
4453
  }
3725
4454
  function handleHelpKey(state, scroll, key) {
4455
+ const page = pageNotch(bodyRows(state));
3726
4456
  const move = {
3727
4457
  down: 1,
3728
4458
  up: -1,
3729
- pagedown: 10,
3730
- pageup: -10,
3731
- scrolldown: 3,
3732
- scrollup: -3
4459
+ pagedown: page,
4460
+ pageup: -page,
4461
+ scrolldown: WHEEL_NOTCH,
4462
+ scrollup: -WHEEL_NOTCH
3733
4463
  };
3734
4464
  const delta = move[key.name];
3735
4465
  if (delta === void 0) return step({ ...state, overlay: null });
3736
- return step({ ...state, overlay: { kind: "help", scroll: Math.max(0, scroll + delta) } });
4466
+ return step({ ...state, overlay: { kind: "help", scroll: clamp(scroll + delta, helpScrollMax(state)) } });
3737
4467
  }
3738
4468
  function handlePromptKey(state, overlay, key) {
3739
4469
  if (key.name === "enter") {
@@ -3756,18 +4486,13 @@ function editText(value, key) {
3756
4486
  if (key.name === "ctrl-u") return "";
3757
4487
  return value;
3758
4488
  }
3759
- function visibleItems(picker2) {
3760
- const filter = picker2.filter.trim().toLowerCase();
3761
- if (!filter) return picker2.items;
3762
- return picker2.items.filter(
3763
- (i) => i.id.toLowerCase().includes(filter) || i.label.toLowerCase().includes(filter) || (i.detail ?? "").toLowerCase().includes(filter)
3764
- );
3765
- }
3766
4489
  function handlePickerKey(state, picker2, key) {
3767
4490
  const items = visibleItems(picker2);
3768
4491
  const reopen = (next) => step({ ...state, overlay: { kind: "picker", picker: next } });
3769
- if (key.name === "up") return reopen({ ...picker2, index: Math.max(0, picker2.index - 1) });
3770
- if (key.name === "down") return reopen({ ...picker2, index: Math.min(items.length - 1, picker2.index + 1) });
4492
+ if (key.name === "up" || key.name === "scrollup") return reopen({ ...picker2, index: Math.max(0, picker2.index - 1) });
4493
+ if (key.name === "down" || key.name === "scrolldown") {
4494
+ return reopen({ ...picker2, index: Math.min(items.length - 1, picker2.index + 1) });
4495
+ }
3771
4496
  if (picker2.multi && key.name === "char" && key.char === " ") {
3772
4497
  const item = items[picker2.index];
3773
4498
  if (!item) return reopen(picker2);
@@ -3813,11 +4538,8 @@ function choose(state, picker2, item) {
3813
4538
  switch (picker2.action.kind) {
3814
4539
  case "set-model":
3815
4540
  return step(closed, [{ type: "setModel", modelId: item.id }]);
3816
- case "set-planner": {
3817
- const runner = (0, import_core7.runnerForProvider)(item.id);
3818
- const stillValid = runner ? state.models.some((m) => m.id === state.orchestratorModel && m.runners?.includes(runner)) : state.orchestratorModels.some((m) => m.id === state.orchestratorModel);
3819
- return step(closed, [{ type: "setPlanner", provider: item.id, clearModel: !stillValid }]);
3820
- }
4541
+ case "set-planner":
4542
+ return step(closed, [{ type: "setPlanner", provider: item.id }]);
3821
4543
  case "set-planner-effort":
3822
4544
  return step(closed, [{ type: "setPlannerEffort", effort: item.id === DEFAULT_EFFORT ? "" : item.id }]);
3823
4545
  case "load-session":
@@ -3911,7 +4633,12 @@ function runCommand(state, { name, args }) {
3911
4633
  (sessionId) => step({ ...state, planApproved: true }, [{ type: "execute", sessionId }])
3912
4634
  );
3913
4635
  case "stop":
3914
- return withSession(state, (sessionId) => step(state, [{ type: "stopExecution", sessionId }]));
4636
+ return withSession(
4637
+ state,
4638
+ (sessionId) => step(state, [
4639
+ state.status === "planning" || state.status === "researching" ? { type: "cancelPlanning", sessionId } : { type: "stopExecution", sessionId }
4640
+ ])
4641
+ );
3915
4642
  case "model":
3916
4643
  return setModel(state, args);
3917
4644
  case "planner":
@@ -3974,984 +4701,481 @@ function runCommand(state, { name, args }) {
3974
4701
  case "skip":
3975
4702
  case "retry":
3976
4703
  case "cancel":
3977
- case "force-start":
3978
- return taskCommand(
3979
- state,
3980
- args[0],
3981
- (sessionId, taskId) => step(state, [taskActionEffect(state, sessionId, taskId, name)])
3982
- );
3983
- }
3984
- return fail2(state, `/${name} is not wired up yet.`);
3985
- }
3986
- function picker(title, items, action, extra = {}) {
3987
- return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
3988
- }
3989
- function withSession(state, run) {
3990
- if (!state.sessionId) {
3991
- return fail2(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
3992
- }
3993
- return run(state.sessionId);
3994
- }
3995
- function resolveToggle(arg, current) {
3996
- const value = arg?.toLowerCase();
3997
- if (value === "on") return true;
3998
- if (value === "off") return false;
3999
- if (value === void 0) return !current;
4000
- return null;
4001
- }
4002
- function toggleSkill(state, skill, arg) {
4003
- const enabled = resolveToggle(arg, state.skills[skill]);
4004
- if (enabled === null) return fail2(state, `Usage: /${skill} [on|off]`);
4005
- return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
4006
- }
4007
- function requestNewSession(state) {
4008
- const hasContent = state.tasks.length > 0 || state.messages.length > 0 || state.goal !== "" || state.status !== "idle";
4009
- if (!state.sessionId || !hasContent) return newSession(state);
4010
- return step({
4011
- ...state,
4012
- overlay: {
4013
- kind: "confirm",
4014
- title: "Start a new session?",
4015
- message: "The current plan will be cleared and any running tasks stopped.",
4016
- action: { kind: "new-session" }
4017
- }
4018
- });
4019
- }
4020
- function newSession(state) {
4021
- const closeEffects = state.sessionId ? [{ type: "closeSession", sessionId: state.sessionId }] : [];
4022
- return step({
4023
- ...state,
4024
- sessionId: null,
4025
- goal: "",
4026
- tasks: [],
4027
- planApproved: false,
4028
- messages: [],
4029
- selectedTask: 0,
4030
- expandedTaskId: null,
4031
- taskEditor: null,
4032
- scroll: 0,
4033
- status: "idle",
4034
- busyLabel: "",
4035
- thinkingLine: "",
4036
- // Prompts belong to the session that raised them; the old planner is gone
4037
- // and its pending requests deny on their own timeout.
4038
- pendingApprovals: [],
4039
- overlay: state.overlay?.kind === "approval" ? null : state.overlay
4040
- }, closeEffects);
4041
- }
4042
- function setModel(state, args) {
4043
- if (args[0] === "set" && args[1]) {
4044
- const runner = (0, import_core7.runnerForProvider)(state.plannerProvider);
4045
- const known = runner ? modelsForRunner(state.models, runner) : [];
4046
- if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
4047
- return fail2(state, `${args[1]} was not discovered for ${runner}.`);
4048
- }
4049
- return step(state, [{ type: "setModel", modelId: args[1] }]);
4050
- }
4051
- if (args.length > 0 && args[0] !== "set") return fail2(state, "Usage: /model [set <model-id>]");
4052
- const items = pickerItemsFor(state, { kind: "set-model" });
4053
- return step(
4054
- {
4055
- ...state,
4056
- overlay: {
4057
- kind: "picker",
4058
- picker: picker("Orchestrator model", items, { kind: "set-model" }, { hint: providerErrorHint(state) })
4059
- }
4060
- },
4061
- [{ type: "loadModels" }]
4062
- );
4063
- }
4064
- function plannerItems(state) {
4065
- const installed = new Set(state.runners.map((r) => r.id));
4066
- const agents = import_core7.CLI_PROVIDERS.map((id) => {
4067
- const runner = (0, import_core7.runnerForProvider)(id);
4068
- const ready = installed.has(runner);
4069
- return {
4070
- id,
4071
- label: import_core7.ALL_PROVIDERS[id].label,
4072
- detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
4073
- selected: state.plannerProvider === id,
4074
- disabled: !ready
4075
- };
4076
- });
4077
- const vendors = import_core7.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4078
- id,
4079
- label: import_core7.ALL_PROVIDERS[id].label,
4080
- detail: import_core7.ALL_PROVIDERS[id].apiKeyEnvVar,
4081
- selected: state.plannerProvider === id
4082
- }));
4083
- return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
4084
- }
4085
- function setPlanner(state, args) {
4086
- if (args[0]) {
4087
- const id = args[0].toLowerCase();
4088
- if (!KNOWN_PROVIDERS2.includes(id)) {
4089
- return fail2(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
4090
- }
4091
- return step(state, [{ type: "setPlanner", provider: id }]);
4092
- }
4093
- return step({
4094
- ...state,
4095
- overlay: {
4096
- kind: "picker",
4097
- picker: picker("Planner", plannerItems(state), { kind: "set-planner" }, {
4098
- hint: "Who researches your goal and writes the plan. Coding agents use their own subscription \u2014 no API key."
4099
- })
4100
- }
4101
- });
4102
- }
4103
- function setPlannerEffort(state, args) {
4104
- const items = plannerEffortItems(state);
4105
- if (args[0]) {
4106
- const wanted = args[0].toLowerCase();
4107
- const match = items.find((i) => !i.disabled && (i.id === wanted || wanted === "default" && i.id === DEFAULT_EFFORT));
4108
- if (!match) {
4109
- const available = items.filter((i) => !i.disabled && i.id !== DEFAULT_EFFORT).map((i) => i.id);
4110
- return fail2(state, available.length > 0 ? `Unknown effort: ${args[0]}. Available: ${available.join(", ")}, default.` : items[0]?.label ?? "No effort levels available.");
4111
- }
4112
- return step(state, [{ type: "setPlannerEffort", effort: match.id === DEFAULT_EFFORT ? "" : match.id }]);
4113
- }
4114
- return step({
4115
- ...state,
4116
- overlay: {
4117
- kind: "picker",
4118
- picker: picker("Planner thinking effort", items, { kind: "set-planner-effort" }, {
4119
- hint: "How hard the planning agent thinks per turn. Higher costs latency and tokens against your subscription."
4120
- })
4121
- }
4122
- });
4123
- }
4124
- function setKey(state, args) {
4125
- if (args[0] === "set" && args[1]) {
4126
- const provider = args[1].toLowerCase();
4127
- if (!KNOWN_PROVIDERS2.includes(provider)) {
4128
- return fail2(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
4129
- }
4130
- if (!args[2]) {
4131
- return step({
4132
- ...state,
4133
- overlay: keyPrompt(provider)
4134
- });
4135
- }
4136
- return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
4137
- }
4138
- const items = import_core7.PROVIDER_PRIORITY.map((id) => ({
4139
- id,
4140
- label: import_core7.ALL_PROVIDERS[id].label,
4141
- detail: import_core7.ALL_PROVIDERS[id].apiKeyEnvVar,
4142
- selected: state.configuredProviders.includes(id)
4143
- }));
4144
- return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
4145
- }
4146
- function keyPrompt(provider) {
4147
- const meta = import_core7.ALL_PROVIDERS[provider];
4148
- return {
4149
- kind: "prompt",
4150
- title: `${meta.label} API key`,
4151
- hint: `Stored as ${meta.apiKeyEnvVar} in your .env`,
4152
- value: "",
4153
- action: { kind: "api-key", provider, envVar: meta.apiKeyEnvVar }
4154
- };
4155
- }
4156
- function allowlist(state, args) {
4157
- const [sub, runner, ...rest] = args;
4158
- if (sub === "set") {
4159
- if (!runner || rest.length === 0) return fail2(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
4160
- if (state.runners.length > 0 && !state.runners.some((r) => r.id === runner)) {
4161
- return fail2(state, `Unknown runner "${runner}".`);
4162
- }
4163
- const modelIds = rest.join(" ").split(",").map((s) => s.trim()).filter(Boolean);
4164
- if (modelIds.length === 0) return fail2(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
4165
- const known = modelsForRunner(state.models, runner);
4166
- if (state.models.length > 0 && known.length > 0) {
4167
- const stray = modelIds.filter((id) => !known.some((m) => m.id === id));
4168
- if (stray.length > 0) {
4169
- return fail2(state, `Not discovered for ${runner}: ${stray.join(", ")}.`);
4170
- }
4171
- }
4172
- return step(state, [{ type: "setAllowlist", runner, modelIds }]);
4173
- }
4174
- if (sub === "clear") {
4175
- if (!runner) return fail2(state, "Usage: /allowlist clear <runner>");
4176
- return step(state, [{ type: "setAllowlist", runner, modelIds: [] }]);
4177
- }
4178
- if (sub === "show" || sub === void 0) {
4179
- const items = state.runners.map((r) => ({
4180
- id: r.id,
4181
- label: r.name,
4182
- detail: describeAllowlist(state.allowlist[r.id])
4183
- }));
4184
- return step(
4185
- {
4186
- ...state,
4187
- overlay: {
4188
- kind: "picker",
4189
- picker: picker("Limit models for which runner?", items, { kind: "choose-allowlist-runner" })
4190
- }
4191
- },
4192
- // The model list the next picker is built from must be the runner's real
4193
- // one; refresh it while the user is still choosing a runner.
4194
- [{ type: "loadModels" }]
4195
- );
4196
- }
4197
- return fail2(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
4198
- }
4199
- function describeAllowlist(ids) {
4200
- if (!ids || ids.length === 0) return "no restriction";
4201
- return `${ids.length} model${ids.length === 1 ? "" : "s"} allowed`;
4202
- }
4203
- function runners(state, args) {
4204
- const [runner, arg] = args;
4205
- if (runner) {
4206
- const known = state.runners.find((r) => r.id === runner);
4207
- const enabled = resolveToggle(arg, known?.enabled ?? true);
4208
- if (enabled === null) return fail2(state, "Usage: /runners [<runner-id> on|off]");
4209
- return step(state, [{ type: "setRunnerEnabled", runner, enabled }]);
4210
- }
4211
- const action = { kind: "set-runners" };
4212
- const items = pickerItemsFor(state, action);
4213
- return step({ ...state, overlay: {
4214
- kind: "picker",
4215
- picker: picker("Runners", items, action, {
4216
- hint: "Enabled runners are the ones the planner may assign work to.",
4217
- multi: true,
4218
- chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
4219
- })
4220
- } });
4704
+ case "force-start":
4705
+ return taskCommand(
4706
+ state,
4707
+ args[0],
4708
+ (sessionId, taskId) => step(state, [taskActionEffect(state, sessionId, taskId, name)])
4709
+ );
4710
+ }
4711
+ return fail2(state, `/${name} is not wired up yet.`);
4221
4712
  }
4222
- function setAutonomous(state, arg) {
4223
- const enabled = resolveToggle(arg, state.autonomous);
4224
- if (enabled === null) return fail2(state, "Usage: /auto [on|off]");
4225
- return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
4713
+ function picker(title, items, action, extra = {}) {
4714
+ return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
4226
4715
  }
4227
- function setMouseCapture(state, arg) {
4228
- const enabled = resolveToggle(arg, state.mouseCapture);
4229
- if (enabled === null) return fail2(state, "Usage: /mouse [on|off]");
4230
- const noted = say(
4231
- { ...state, mouseCapture: enabled },
4232
- "system",
4233
- enabled ? "Mouse capture on \u2014 the wheel scrolls, but dragging no longer selects text." : "Mouse capture off \u2014 drag to select and copy; scroll with pgup/pgdn."
4234
- );
4235
- return step(noted, [{ type: "setMouseCapture", enabled }]);
4716
+ function withSession(state, run) {
4717
+ if (!state.sessionId) {
4718
+ return fail2(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
4719
+ }
4720
+ return run(state.sessionId);
4236
4721
  }
4237
- function openTaskRunnerPicker(state, task) {
4238
- if (task.type !== "ai") return fail2(state, "Manual tasks do not run on an executor, so they have no runner.");
4239
- const action = { kind: "set-task-runner", taskId: task.id };
4240
- return step(
4241
- {
4242
- ...state,
4243
- overlay: {
4244
- kind: "picker",
4245
- picker: picker(`Runner \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
4246
- hint: "Changing the runner re-picks this task model, effort and mode for it."
4247
- })
4248
- }
4249
- }
4250
- );
4722
+ function resolveToggle(arg, current) {
4723
+ const value = arg?.toLowerCase();
4724
+ if (value === "on") return true;
4725
+ if (value === "off") return false;
4726
+ if (value === void 0) return !current;
4727
+ return null;
4251
4728
  }
4252
- function confirmRemoveTask(state, task) {
4253
- const dependents = (0, import_plan_utils3.dependentsOf)(state.tasks, task.id);
4254
- const named = dependents.map((t) => `#${t.order} ${t.title}`).join(", ");
4729
+ function toggleSkill(state, skill, arg) {
4730
+ const enabled = resolveToggle(arg, state.skills[skill]);
4731
+ if (enabled === null) return fail2(state, `Usage: /${skill} [on|off]`);
4732
+ return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
4733
+ }
4734
+ function requestNewSession(state) {
4735
+ const hasContent = state.tasks.length > 0 || state.messages.length > 0 || state.goal !== "" || state.status !== "idle";
4736
+ if (!state.sessionId || !hasContent) return newSession(state);
4255
4737
  return step({
4256
4738
  ...state,
4257
4739
  overlay: {
4258
4740
  kind: "confirm",
4259
- title: `Remove #${task.order} ${task.title}?`,
4260
- message: dependents.length > 0 ? `${dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`} on it and will lose that dependency: ${named}.` : "This cannot be undone.",
4261
- action: { kind: "remove-task", taskId: task.id }
4741
+ title: "Start a new session?",
4742
+ message: "The current plan will be cleared and any running tasks stopped.",
4743
+ action: { kind: "new-session" }
4262
4744
  }
4263
4745
  });
4264
4746
  }
4265
- function openTaskDepsPicker(state, task) {
4266
- const action = { kind: "set-task-deps", taskId: task.id };
4267
- const items = pickerItemsFor(state, action);
4268
- if (items.length === 0) return fail2(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
4747
+ function newSession(state) {
4748
+ const closeEffects = state.sessionId ? [{ type: "closeSession", sessionId: state.sessionId }] : [];
4269
4749
  return step({
4270
4750
  ...state,
4271
- overlay: {
4272
- kind: "picker",
4273
- picker: picker(`Depends on \xB7 #${task.order} ${task.title}`, items, action, {
4274
- hint: "Only tasks earlier in the plan can be dependencies.",
4275
- multi: true,
4276
- chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
4277
- })
4278
- }
4279
- });
4751
+ sessionId: null,
4752
+ goal: "",
4753
+ tasks: [],
4754
+ planApproved: false,
4755
+ messages: [],
4756
+ selectedTask: 0,
4757
+ expandedTaskId: null,
4758
+ taskEditor: null,
4759
+ scroll: 0,
4760
+ status: "idle",
4761
+ busyLabel: "",
4762
+ thinkingLine: "",
4763
+ // Prompts belong to the session that raised them; the old planner is gone
4764
+ // and its pending requests deny on their own timeout.
4765
+ pendingApprovals: [],
4766
+ overlay: state.overlay?.kind === "approval" ? null : state.overlay
4767
+ }, closeEffects);
4280
4768
  }
4281
- function openTaskModePicker(state, task) {
4282
- if (task.type !== "ai") return fail2(state, "Manual tasks do not have an executor mode.");
4283
- const modes = modesForTask(state.modesByRunner, task);
4284
- if (modes.length === 0) {
4285
- return fail2(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
4286
- }
4287
- const action = { kind: "set-task-mode", taskId: task.id };
4288
- return step(
4289
- {
4290
- ...state,
4291
- overlay: {
4292
- kind: "picker",
4293
- picker: picker(`Mode \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
4294
- hint: `Modes declared by ${task.assignedRunner}.`
4295
- })
4296
- }
4769
+ function setModel(state, args) {
4770
+ if (args[0] === "set" && args[1]) {
4771
+ const runner = (0, import_core8.runnerForProvider)(state.plannerProvider);
4772
+ const known = runner ? modelsForRunner(state.models, runner) : [];
4773
+ if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
4774
+ return fail2(state, `${args[1]} was not discovered for ${runner}.`);
4297
4775
  }
4298
- );
4299
- }
4300
- function openTaskModelPicker(state, task) {
4301
- if (task.type !== "ai") return fail2(state, "Manual tasks do not have an executor model.");
4302
- const action = { kind: "set-task-model", taskId: task.id };
4303
- return step(
4304
- {
4305
- ...state,
4306
- overlay: {
4307
- kind: "picker",
4308
- picker: picker(`Model \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
4309
- hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
4310
- })
4311
- }
4312
- },
4313
- [{ type: "loadModels" }]
4314
- );
4315
- }
4316
- function openTaskEffortPicker(state, task) {
4317
- if (task.type !== "ai") return fail2(state, "Manual tasks do not have a thinking effort.");
4318
- if (!task.assignedModel) return fail2(state, "Choose a model for this task before setting its thinking effort.");
4319
- const action = { kind: "set-task-effort", taskId: task.id };
4776
+ return step(state, [{ type: "setModel", modelId: args[1] }]);
4777
+ }
4778
+ if (args.length > 0 && args[0] !== "set") return fail2(state, "Usage: /model [set <model-id>]");
4779
+ const items = pickerItemsFor(state, { kind: "set-model" });
4320
4780
  return step(
4321
4781
  {
4322
4782
  ...state,
4323
4783
  overlay: {
4324
4784
  kind: "picker",
4325
- picker: picker(`Thinking effort \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
4326
- hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
4327
- })
4785
+ picker: picker("Orchestrator model", items, { kind: "set-model" }, { hint: providerErrorHint(state) })
4328
4786
  }
4329
4787
  },
4330
4788
  [{ type: "loadModels" }]
4331
4789
  );
4332
4790
  }
4333
- function assignTaskModel(state, sessionId, task, model) {
4334
- const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
4335
- return step(state, [{
4336
- type: "updateTask",
4337
- sessionId,
4338
- taskId: task.id,
4339
- // JSON drops `undefined`; null is intentional here so changing models can
4340
- // also clear a stale legacy top-level effort on the persisted task.
4341
- changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
4342
- message: `Task #${task.order} model set to ${model.label}.`
4343
- }]);
4344
- }
4345
- function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
4346
- return step(state, [{
4347
- type: "updateTask",
4348
- sessionId,
4349
- taskId: task.id,
4350
- changes: { assignedRunner: runner },
4351
- message: `Task #${task.order} runner set to ${runnerLabel}.`
4352
- }]);
4353
- }
4354
- function assignTaskMode(state, sessionId, task, mode, modeLabel) {
4355
- return step(state, [{
4356
- type: "updateTask",
4357
- sessionId,
4358
- taskId: task.id,
4359
- changes: { taskMode: mode },
4360
- message: `Task #${task.order} mode set to ${modeLabel}.`
4361
- }]);
4362
- }
4363
- function assignTaskEffort(state, sessionId, task, thinkingEffort) {
4364
- const assignedModel = task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0;
4365
- return step(state, [{
4366
- type: "updateTask",
4367
- sessionId,
4368
- taskId: task.id,
4369
- changes: { assignedModel, thinkingEffort: thinkingEffort ?? null },
4370
- message: `Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`
4371
- }]);
4372
- }
4373
- function taskModelCommand(state, args) {
4374
- return taskCommand(state, args[0], (sessionId, taskId) => {
4375
- const task = state.tasks.find((candidate) => candidate.id === taskId);
4376
- if (!args[1]) return openTaskModelPicker(state, task);
4377
- const model = state.models.find((candidate) => candidate.id === args[1]) ?? {
4378
- id: args[1],
4379
- label: args[1],
4380
- provider: "",
4381
- variants: []
4791
+ function plannerItems(state) {
4792
+ const installed = new Set(state.runners.map((r) => r.id));
4793
+ const agents = import_core8.CLI_PROVIDERS.map((id) => {
4794
+ const runner = (0, import_core8.runnerForProvider)(id);
4795
+ const ready = installed.has(runner);
4796
+ return {
4797
+ id,
4798
+ label: import_core8.ALL_PROVIDERS[id].label,
4799
+ detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
4800
+ selected: state.plannerProvider === id,
4801
+ disabled: !ready
4382
4802
  };
4383
- if (!runnerAccepts(task, model)) {
4384
- return fail2(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
4385
- }
4386
- return assignTaskModel(state, sessionId, task, model);
4387
- });
4388
- }
4389
- function taskRunnerCommand(state, args) {
4390
- return taskCommand(state, args[0], (sessionId, taskId) => {
4391
- const task = state.tasks.find((candidate) => candidate.id === taskId);
4392
- if (!args[1]) return openTaskRunnerPicker(state, task);
4393
- const runner = state.runners.find((candidate) => candidate.id === args[1]);
4394
- if (!runner) return fail2(state, `Unknown runner "${args[1]}".`);
4395
- return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
4396
4803
  });
4804
+ const vendors = import_core8.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4805
+ id,
4806
+ label: import_core8.ALL_PROVIDERS[id].label,
4807
+ detail: import_core8.ALL_PROVIDERS[id].apiKeyEnvVar,
4808
+ selected: state.plannerProvider === id
4809
+ }));
4810
+ return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
4397
4811
  }
4398
- function taskModeCommand(state, args) {
4399
- return taskCommand(state, args[0], (sessionId, taskId) => {
4400
- const task = state.tasks.find((candidate) => candidate.id === taskId);
4401
- if (!args[1]) return openTaskModePicker(state, task);
4402
- const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
4403
- if (!mode) return fail2(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
4404
- return assignTaskMode(state, sessionId, task, mode.id, mode.label);
4812
+ function setPlanner(state, args) {
4813
+ if (args[0]) {
4814
+ const id = args[0].toLowerCase();
4815
+ if (!KNOWN_PROVIDERS2.includes(id)) {
4816
+ return fail2(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
4817
+ }
4818
+ return step(state, [{ type: "setPlanner", provider: id }]);
4819
+ }
4820
+ return step({
4821
+ ...state,
4822
+ overlay: {
4823
+ kind: "picker",
4824
+ picker: picker("Planner", plannerItems(state), { kind: "set-planner" }, {
4825
+ hint: "Who researches your goal and writes the plan. Coding agents use their own subscription \u2014 no API key."
4826
+ })
4827
+ }
4405
4828
  });
4406
4829
  }
4407
- function taskEffortCommand(state, args) {
4408
- return taskCommand(state, args[0], (sessionId, taskId) => {
4409
- const task = state.tasks.find((candidate) => candidate.id === taskId);
4410
- if (!args[1]) return openTaskEffortPicker(state, task);
4411
- if (!task.assignedModel) return fail2(state, "Choose a model for this task before setting its thinking effort.");
4412
- const effort = args[1].toLowerCase();
4413
- const value = effort === "default" || effort === "none" ? void 0 : args[1];
4414
- const supported = effortsForTask(state.models, task);
4415
- if (value && supported.length > 0 && !supported.some((variant) => variant.id === value)) {
4416
- return fail2(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
4830
+ function setPlannerEffort(state, args) {
4831
+ const items = plannerEffortItems(state);
4832
+ if (args[0]) {
4833
+ const wanted = args[0].toLowerCase();
4834
+ const match = items.find((i) => !i.disabled && (i.id === wanted || wanted === "default" && i.id === DEFAULT_EFFORT));
4835
+ if (!match) {
4836
+ const available = items.filter((i) => !i.disabled && i.id !== DEFAULT_EFFORT).map((i) => i.id);
4837
+ return fail2(state, available.length > 0 ? `Unknown effort: ${args[0]}. Available: ${available.join(", ")}, default.` : items[0]?.label ?? "No effort levels available.");
4838
+ }
4839
+ return step(state, [{ type: "setPlannerEffort", effort: match.id === DEFAULT_EFFORT ? "" : match.id }]);
4840
+ }
4841
+ return step({
4842
+ ...state,
4843
+ overlay: {
4844
+ kind: "picker",
4845
+ picker: picker("Planner thinking effort", items, { kind: "set-planner-effort" }, {
4846
+ hint: "How hard the planning agent thinks per turn. Higher costs latency and tokens against your subscription."
4847
+ })
4417
4848
  }
4418
- return assignTaskEffort(state, sessionId, task, value);
4419
4849
  });
4420
4850
  }
4421
- function addTask(state, title) {
4422
- return withSession(state, (sessionId) => {
4423
- if (!title) {
4851
+ function setKey(state, args) {
4852
+ if (args[0] === "set" && args[1]) {
4853
+ const provider = args[1].toLowerCase();
4854
+ if (!KNOWN_PROVIDERS2.includes(provider)) {
4855
+ return fail2(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
4856
+ }
4857
+ if (!args[2]) {
4424
4858
  return step({
4425
4859
  ...state,
4426
- overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
4860
+ overlay: keyPrompt(provider)
4427
4861
  });
4428
4862
  }
4429
- return step(state, [{ type: "addTask", sessionId, title }]);
4430
- });
4431
- }
4432
- function taskCommand(state, token, run) {
4433
- return withSession(state, (sessionId) => {
4434
- if (!token) return fail2(state, "Which task? Pass a task id or its number in the plan.");
4435
- const taskId = resolveTaskId2(state, token);
4436
- if (!taskId) return fail2(state, `No task matching "${token}" in the current plan.`);
4437
- return run(sessionId, taskId);
4438
- });
4439
- }
4440
- function resolveTaskId2(state, token) {
4441
- const byId = state.tasks.find((t) => t.id === token);
4442
- if (byId) return byId.id;
4443
- const order = Number(token);
4444
- if (Number.isInteger(order)) {
4445
- const byOrder = state.tasks.find((t) => t.order === order);
4446
- if (byOrder) return byOrder.id;
4863
+ return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
4447
4864
  }
4448
- return null;
4865
+ const items = import_core8.PROVIDER_PRIORITY.map((id) => ({
4866
+ id,
4867
+ label: import_core8.ALL_PROVIDERS[id].label,
4868
+ detail: import_core8.ALL_PROVIDERS[id].apiKeyEnvVar,
4869
+ selected: state.configuredProviders.includes(id)
4870
+ }));
4871
+ return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
4449
4872
  }
4450
-
4451
- // src/tui/render.ts
4452
- var import_core8 = require("@ordewell/core");
4453
- var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4454
- var CHROME_ROWS = 4;
4455
- function render(state) {
4456
- const { rows, cols } = state;
4457
- const input = renderInput(state, cols);
4458
- const footer = renderFooter(state, cols);
4459
- const bodyRows = Math.max(0, rows - CHROME_ROWS - input.length - footer.length + 2);
4460
- const body = state.overlay ? renderOverlay(state, bodyRows, cols) : renderBody(state, bodyRows, cols);
4461
- const lines = [
4462
- renderSkills(state, cols),
4463
- ...body,
4464
- renderStatus(state, cols),
4465
- ...input,
4466
- ...footer
4467
- ];
4468
- return lines.slice(Math.max(0, lines.length - rows)).map((l) => pad(l, cols));
4873
+ function keyPrompt(provider) {
4874
+ const meta = import_core8.ALL_PROVIDERS[provider];
4875
+ return {
4876
+ kind: "prompt",
4877
+ title: `${meta.label} API key`,
4878
+ hint: `Stored as ${meta.apiKeyEnvVar} in your .env`,
4879
+ value: "",
4880
+ action: { kind: "api-key", provider, envVar: meta.apiKeyEnvVar }
4881
+ };
4469
4882
  }
4470
- function fit(lines, rows, anchor) {
4471
- if (lines.length >= rows) {
4472
- return anchor === "bottom" ? lines.slice(lines.length - rows) : lines.slice(0, rows);
4883
+ function allowlist(state, args) {
4884
+ const [sub, runner, ...rest] = args;
4885
+ if (sub === "set") {
4886
+ if (!runner || rest.length === 0) return fail2(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
4887
+ if (state.runners.length > 0 && !state.runners.some((r) => r.id === runner)) {
4888
+ return fail2(state, `Unknown runner "${runner}".`);
4889
+ }
4890
+ const modelIds = rest.join(" ").split(",").map((s) => s.trim()).filter(Boolean);
4891
+ if (modelIds.length === 0) return fail2(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
4892
+ const known = modelsForRunner(state.models, runner);
4893
+ if (state.models.length > 0 && known.length > 0) {
4894
+ const stray = modelIds.filter((id) => !known.some((m) => m.id === id));
4895
+ if (stray.length > 0) {
4896
+ return fail2(state, `Not discovered for ${runner}: ${stray.join(", ")}.`);
4897
+ }
4898
+ }
4899
+ return step(state, [{ type: "setAllowlist", runner, modelIds }]);
4473
4900
  }
4474
- const blanks = Array(rows - lines.length).fill("");
4475
- return anchor === "bottom" ? [...blanks, ...lines] : [...lines, ...blanks];
4476
- }
4477
- function renderSkills(state, cols) {
4478
- const badges = SKILL_IDS.map(
4479
- (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
4480
- );
4481
- const auto = state.autonomous ? style.yellow("\u25CF auto") : style.grey("\u25CB auto");
4482
- return truncate([...badges, auto].join(" "), cols);
4483
- }
4484
- function renderStatus(state, cols) {
4485
- if (state.status === "idle") {
4486
- return state.toast ? truncate(style.grey(state.toast), cols) : "";
4901
+ if (sub === "clear") {
4902
+ if (!runner) return fail2(state, "Usage: /allowlist clear <runner>");
4903
+ return step(state, [{ type: "setAllowlist", runner, modelIds: [] }]);
4487
4904
  }
4488
- const label = state.busyLabel ? ` ${state.busyLabel}` : "";
4489
- const verb = state.status === "executing" ? "Executing" : state.status === "researching" ? "Researching" : "Planning";
4490
- const head = style.yellow(`\u25CF ${verb}\u2026${label}`);
4491
- const thinking = state.thinkingLine.replace(/\s+/g, " ").trim();
4492
- if (!thinking) return truncate(head, cols);
4493
- const room = cols - width(head) - 3;
4494
- if (room < 12) return truncate(head, cols);
4495
- const tail = thinking.length > room ? `\u2026${thinking.slice(-(room - 1)).trimStart()}` : thinking;
4496
- return truncate(`${head}${style.grey(` \xB7 ${tail}`)}`, cols);
4497
- }
4498
- function renderInput(state, cols) {
4499
- const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
4500
- const active = state.focus === "chat" && !state.overlay;
4501
- const room = chatEditorRoom(cols, active);
4502
- const { text, cursor } = state.editor;
4503
- const continuation = " ".repeat(width(prompt));
4504
- if (!active) {
4505
- const visible = text.replace(/\n/g, "\xB6").replace(/\t/g, " ");
4506
- return [prompt + visible + suggest(state, room - width(visible))];
4905
+ if (sub === "show" || sub === void 0) {
4906
+ const items = state.runners.map((r) => ({
4907
+ id: r.id,
4908
+ label: r.name,
4909
+ detail: describeAllowlist(state.allowlist[r.id])
4910
+ }));
4911
+ return step(
4912
+ {
4913
+ ...state,
4914
+ overlay: {
4915
+ kind: "picker",
4916
+ picker: picker("Limit models for which runner?", items, { kind: "choose-allowlist-runner" })
4917
+ }
4918
+ },
4919
+ // The model list the next picker is built from must be the runner's real
4920
+ // one; refresh it while the user is still choosing a runner.
4921
+ [{ type: "loadModels" }]
4922
+ );
4507
4923
  }
4508
- const lines = wrapLines(text, room);
4509
- const { line: lineIndex, col } = cursorInLines(lines, cursor, text.length);
4510
- const rawLines = lines.map((l) => l.line);
4511
- return rawLines.map((raw, i) => {
4512
- const prefix = i === 0 ? prompt : continuation;
4513
- const line = raw.replace(/\t/g, " ");
4514
- const isCursorLine = i === lineIndex;
4515
- const rendered = isCursorLine ? line.slice(0, col) + style.inverse(line.slice(col, col + 1) || " ") + line.slice(col + 1) : line;
4516
- const used = width(line) + (isCursorLine && col >= line.length ? 1 : 0);
4517
- const hint = i === rawLines.length - 1 ? suggest(state, room - used) : "";
4518
- return prefix + rendered + hint;
4519
- });
4520
- }
4521
- function suggest(state, room) {
4522
- if (state.focus === "plan" || room <= 2) return "";
4523
- const matches = completions(state.editor.text);
4524
- if (matches.length === 0) return "";
4525
- const names = matches.map((c) => c.name).join(" ");
4526
- return style.grey(` ${truncate(names, Math.max(0, room - 2))}`);
4924
+ return fail2(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
4527
4925
  }
4528
- function renderFooter(state, cols) {
4529
- const markHint = state.tasks[state.selectedTask]?.status === "completed" ? "m undone" : "m done";
4530
- const hints = state.focus === "plan" ? state.expandedTaskId ? ["type to edit prompt", "pgup/pgdn scroll", "alt-enter newline", "enter save", "esc cancel"] : [
4531
- "enter expand",
4532
- "R runner",
4533
- "o model",
4534
- "e effort",
4535
- "M mode",
4536
- "D deps",
4537
- "f start",
4538
- "E run plan",
4539
- "c cancel",
4540
- markHint,
4541
- "s skip",
4542
- "a add",
4543
- "d remove",
4544
- "t terminal",
4545
- "pgup/pgdn scroll",
4546
- "tab chat"
4547
- ] : ["/help", "tab plan", "alt-enter newline", "pgup/pgdn scroll", "ctrl-c quit"];
4548
- return packHints(hints, cols);
4926
+ function describeAllowlist(ids) {
4927
+ if (!ids || ids.length === 0) return "no restriction";
4928
+ return `${ids.length} model${ids.length === 1 ? "" : "s"} allowed`;
4549
4929
  }
4550
- function packHints(hints, cols) {
4551
- const lines = [];
4552
- let current = "";
4553
- for (const hint of hints) {
4554
- const next = current ? `${current} \xB7 ${hint}` : hint;
4555
- if (current && next.length > cols) {
4556
- lines.push(current);
4557
- current = hint;
4558
- } else {
4559
- current = next;
4560
- }
4930
+ function runners(state, args) {
4931
+ const [runner, arg] = args;
4932
+ if (runner) {
4933
+ const known = state.runners.find((r) => r.id === runner);
4934
+ const enabled = resolveToggle(arg, known?.enabled ?? true);
4935
+ if (enabled === null) return fail2(state, "Usage: /runners [<runner-id> on|off]");
4936
+ return step(state, [{ type: "setRunnerEnabled", runner, enabled }]);
4561
4937
  }
4562
- lines.push(current);
4563
- return lines.map((line) => truncate(style.grey(line), cols));
4938
+ const action = { kind: "set-runners" };
4939
+ const items = pickerItemsFor(state, action);
4940
+ return step({ ...state, overlay: {
4941
+ kind: "picker",
4942
+ picker: picker("Runners", items, action, {
4943
+ hint: "Enabled runners are the ones the planner may assign work to.",
4944
+ multi: true,
4945
+ chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
4946
+ })
4947
+ } });
4564
4948
  }
4565
- function renderBody(state, rows, cols) {
4566
- const planCols = planPaneWidth(state);
4567
- if (planCols === 0) return renderChat(state, rows, cols);
4568
- const chatCols = cols - planCols - 1;
4569
- const chat = renderChat(state, rows, chatCols);
4570
- const plan = renderPlan(state, rows, planCols);
4571
- return Array.from(
4572
- { length: rows },
4573
- (_, i) => `${pad(chat[i] ?? "", chatCols)}${style.grey("\u2502")}${pad(plan[i] ?? "", planCols)}`
4949
+ function setAutonomous(state, arg) {
4950
+ const enabled = resolveToggle(arg, state.autonomous);
4951
+ if (enabled === null) return fail2(state, "Usage: /auto [on|off]");
4952
+ return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
4953
+ }
4954
+ function setMouseCapture(state, arg) {
4955
+ const enabled = resolveToggle(arg, state.mouseCapture);
4956
+ if (enabled === null) return fail2(state, "Usage: /mouse [on|off]");
4957
+ const noted = say(
4958
+ // A selection made under capture is meaningless once the terminal owns the
4959
+ // mouse again, and its highlight would sit on screen with nothing able to
4960
+ // move it.
4961
+ { ...state, mouseCapture: enabled, selection: null },
4962
+ "system",
4963
+ enabled ? "Mouse capture on \u2014 the wheel scrolls, and dragging selects within one pane and copies on release." : "Mouse capture off \u2014 the terminal's own drag-select is back, across both panes; scroll with pgup/pgdn."
4574
4964
  );
4965
+ return step(noted, [{ type: "setMouseCapture", enabled }]);
4575
4966
  }
4576
- var ROLE_PREFIX = {
4577
- user: (t) => `${style.cyan("\u276F")} ${t}`,
4578
- assistant: (t) => `${style.magenta("\u25C6")} ${t}`,
4579
- system: (t) => style.grey(`\xB7 ${t}`),
4580
- error: (t) => `${style.red("\u2717")} ${style.red(t)}`,
4581
- // Research rows go through `researchRow`, which paints the outcome mark
4582
- // separately; this entry only exists to keep the role map total.
4583
- research: (t) => style.grey(` \u21B3 ${t}`)
4584
- };
4585
- var OUTCOME_MARK2 = {
4586
- success: "\u2713",
4587
- failure: "\u2717",
4588
- refused: "\u2298",
4589
- denied: "\u2298",
4590
- not_executed: "\u2013"
4591
- };
4592
- var OUTCOME_PAINT = {
4593
- success: style.grey,
4594
- failure: style.red,
4595
- refused: style.yellow,
4596
- denied: style.yellow,
4597
- not_executed: style.grey
4598
- };
4599
- var RESEARCH_RESULT_CHARS = 90;
4600
- function researchLine(message) {
4601
- const meta = message.research;
4602
- if (!meta?.outcome) return `\u22EF ${message.content}`;
4603
- const result = (meta.result ?? "").replace(/\s+/g, " ").trim();
4604
- const preview = result.length > RESEARCH_RESULT_CHARS ? `${result.slice(0, RESEARCH_RESULT_CHARS)}\u2026` : result;
4605
- const mark = OUTCOME_MARK2[meta.outcome];
4606
- return preview ? `${mark} ${message.content} \u2192 ${preview}` : `${mark} ${message.content}`;
4967
+ function openTaskRunnerPicker(state, task) {
4968
+ if (task.type !== "ai") return fail2(state, "Manual tasks do not run on an executor, so they have no runner.");
4969
+ const action = { kind: "set-task-runner", taskId: task.id };
4970
+ return step(
4971
+ {
4972
+ ...state,
4973
+ overlay: {
4974
+ kind: "picker",
4975
+ picker: picker(`Runner \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
4976
+ hint: "Changing the runner re-picks this task model, effort and mode for it."
4977
+ })
4978
+ }
4979
+ }
4980
+ );
4607
4981
  }
4608
- var RESEARCH_INDENT = " \u21B3 ";
4609
- function researchRow(message, cols) {
4610
- const text = truncate(researchLine(message), Math.max(1, cols - width(RESEARCH_INDENT)));
4611
- const paint = OUTCOME_PAINT[message.research?.outcome ?? "success"];
4612
- const space = text.indexOf(" ");
4613
- if (space < 0) return style.grey(`${RESEARCH_INDENT}${text}`);
4614
- return `${style.grey(" \u21B3")} ${paint(text.slice(0, space))} ${style.grey(text.slice(space + 1))}`;
4982
+ function confirmRemoveTask(state, task) {
4983
+ const dependents = (0, import_plan_utils3.dependentsOf)(state.tasks, task.id);
4984
+ const named = dependents.map((t) => `#${t.order} ${t.title}`).join(", ");
4985
+ return step({
4986
+ ...state,
4987
+ overlay: {
4988
+ kind: "confirm",
4989
+ title: `Remove #${task.order} ${task.title}?`,
4990
+ message: dependents.length > 0 ? `${dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`} on it and will lose that dependency: ${named}.` : "This cannot be undone.",
4991
+ action: { kind: "remove-task", taskId: task.id }
4992
+ }
4993
+ });
4615
4994
  }
4616
- var chatBodyMemo = null;
4617
- function chatBodyLines(messages, cols) {
4618
- if (chatBodyMemo && chatBodyMemo.messages === messages && chatBodyMemo.cols === cols) {
4619
- return chatBodyMemo.lines;
4620
- }
4621
- const lines = [];
4622
- for (const message of messages) {
4623
- if (message.role === "research") {
4624
- lines.push(researchRow(message, cols));
4625
- continue;
4995
+ function openTaskDepsPicker(state, task) {
4996
+ const action = { kind: "set-task-deps", taskId: task.id };
4997
+ const items = pickerItemsFor(state, action);
4998
+ if (items.length === 0) return fail2(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
4999
+ return step({
5000
+ ...state,
5001
+ overlay: {
5002
+ kind: "picker",
5003
+ picker: picker(`Depends on \xB7 #${task.order} ${task.title}`, items, action, {
5004
+ hint: "Only tasks earlier in the plan can be dependencies.",
5005
+ multi: true,
5006
+ chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
5007
+ })
4626
5008
  }
4627
- const wrapped = message.role === "assistant" ? renderMarkdown(message.content, Math.max(1, cols - 2)) : wrap(message.content, Math.max(1, cols - 2));
4628
- lines.push(...wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[message.role](line) : ` ${line}`));
4629
- lines.push("");
4630
- }
4631
- chatBodyMemo = { messages, cols, lines };
4632
- return lines;
5009
+ });
4633
5010
  }
4634
- function renderChat(state, rows, cols) {
4635
- const welcome = state.tasks.length === 0;
4636
- if (welcome) {
4637
- const lines2 = [...renderWelcome(state, cols), ""];
4638
- if (state.messages.length === 0) return fit(lines2.slice(0, -1), rows, "top");
4639
- lines2.push(...chatBodyLines(state.messages, cols));
4640
- const back2 = Math.min(state.scroll, Math.max(0, lines2.length - rows));
4641
- return fit(lines2.slice(0, lines2.length - back2), rows, "bottom");
5011
+ function openTaskModePicker(state, task) {
5012
+ if (task.type !== "ai") return fail2(state, "Manual tasks do not have an executor mode.");
5013
+ const modes = modesForTask(state.modesByRunner, task);
5014
+ if (modes.length === 0) {
5015
+ return fail2(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
4642
5016
  }
4643
- const lines = chatBodyLines(state.messages, cols);
4644
- const back = Math.min(state.scroll, Math.max(0, lines.length - rows));
4645
- return fit(lines.slice(0, lines.length - back), rows, "bottom");
5017
+ const action = { kind: "set-task-mode", taskId: task.id };
5018
+ return step(
5019
+ {
5020
+ ...state,
5021
+ overlay: {
5022
+ kind: "picker",
5023
+ picker: picker(`Mode \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
5024
+ hint: `Modes declared by ${task.assignedRunner}.`
5025
+ })
5026
+ }
5027
+ }
5028
+ );
4646
5029
  }
4647
- function plannerSetup(state) {
4648
- const provider = state.plannerProvider;
4649
- const registration = import_core8.ALL_PROVIDERS[provider];
4650
- if (!registration) return { label: "not set", ready: false };
4651
- const runner = (0, import_core8.runnerForProvider)(provider);
4652
- const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4653
- return { label: registration.label, ready };
5030
+ function openTaskModelPicker(state, task) {
5031
+ if (task.type !== "ai") return fail2(state, "Manual tasks do not have an executor model.");
5032
+ const action = { kind: "set-task-model", taskId: task.id };
5033
+ return step(
5034
+ {
5035
+ ...state,
5036
+ overlay: {
5037
+ kind: "picker",
5038
+ picker: picker(`Model \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
5039
+ hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
5040
+ })
5041
+ }
5042
+ },
5043
+ [{ type: "loadModels" }]
5044
+ );
4654
5045
  }
4655
- var SETUP_KEY_COLS = 10;
4656
- var SETUP_VALUE_COLS = 32;
4657
- function setupRow(label, value, command, cols) {
4658
- const gap = " ".repeat(Math.max(2, SETUP_VALUE_COLS - width(value)));
4659
- const head = ` ${style.grey(pad(label, SETUP_KEY_COLS))}${value}${gap}`;
4660
- return truncate(`${head}${style.grey(command)}`, cols - 1);
5046
+ function openTaskEffortPicker(state, task) {
5047
+ if (task.type !== "ai") return fail2(state, "Manual tasks do not have a thinking effort.");
5048
+ if (!task.assignedModel) return fail2(state, "Choose a model for this task before setting its thinking effort.");
5049
+ const action = { kind: "set-task-effort", taskId: task.id };
5050
+ return step(
5051
+ {
5052
+ ...state,
5053
+ overlay: {
5054
+ kind: "picker",
5055
+ picker: picker(`Thinking effort \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
5056
+ hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
5057
+ })
5058
+ }
5059
+ },
5060
+ [{ type: "loadModels" }]
5061
+ );
4661
5062
  }
4662
- var ICON = `${style.bold("\u226B")}${style.bold(style.cyan("\u25CF"))}`;
4663
- function boldSans(word) {
4664
- return [...word].map((ch) => {
4665
- const code = ch.codePointAt(0);
4666
- return code >= 97 && code <= 122 ? String.fromCodePoint(120302 + (code - 97)) : ch;
4667
- }).join("");
5063
+ function assignTaskModel(state, sessionId, task, model) {
5064
+ const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
5065
+ return step(state, [{
5066
+ type: "updateTask",
5067
+ sessionId,
5068
+ taskId: task.id,
5069
+ // JSON drops `undefined`; null is intentional here so changing models can
5070
+ // also clear a stale legacy top-level effort on the persisted task.
5071
+ changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
5072
+ message: `Task #${task.order} model set to ${model.label}.`
5073
+ }]);
4668
5074
  }
4669
- var WORDMARK = boldSans("ordewell");
4670
- var LOCKUP = `${ICON} ${WORDMARK}`;
4671
- var BANNER_ROWS = [
4672
- { strokes: "\u281A\u283B\u2836\u28A6\u28E4\u2840", dot: "", word: " \u28B8\u28FF\u2847 \u28B8\u28FF \u28FF\u28FF" },
4673
- { strokes: " \u2808\u2819\u28B7\u28E4\u2840", dot: "", word: " \u28C0\u28E4\u28E4\u28E4\u28E4\u2840 \u28E4\u28E4\u2880\u28E4\u2844 \u28C0\u28E4\u28E4\u28E4\u28F8\u28FF\u2847 \u28E0\u28E4\u28E4\u28E4\u28C0 \u28A0\u28E4\u2844 \u28E4\u28E4 \u28E0\u28E4 \u28E0\u28E4\u28E4\u28E4\u28C0 \u28B8\u28FF \u28FF\u28FF" },
4674
- { strokes: "\u28C0\u28C0\u28C0\u28C0\u28C0\u28C0\u28C0\u28C8\u28FB\u28F6\u28E4\u28C0\u28C0", dot: "\u28C0\u28F6\u28FF\u28F7\u2844", word: "\u28F0\u28FF\u280F\u2801\u2808\u28BB\u28FF\u2846 \u28FF\u28FF\u281F\u2809\u2801\u28F0\u28FF\u280F\u2809\u2809\u28BB\u28FF\u2847 \u28FE\u285F\u2801 \u2819\u28FF\u2846 \u28FF\u28E7 \u28B8\u287F\u28FF\u2844 \u28FF\u284F \u28FE\u287F\u2801 \u2819\u28FF\u28C6 \u28B8\u28FF \u28FF\u28FF" },
4675
- { strokes: "\u2809\u2809\u2809\u2809\u2809\u2809\u2809\u2889\u28FD\u287F\u281B\u280B\u2809", dot: "\u2809\u28BF\u28FF\u287F\u2803", word: "\u28FF\u28FF \u28FF\u2847 \u28FF\u28FF \u28FF\u28FF \u28B8\u28FF\u2847\u28B8\u28FF\u287F\u283F\u283F\u283F\u283F\u281F \u28B8\u28FF\u2840\u28FF\u2807\u28B9\u28C7\u28B8\u28FF\u2801 \u28FF\u287F\u283F\u283F\u283F\u283F\u283F \u28B8\u28FF \u28FF\u28FF" },
4676
- { strokes: " \u2880\u28E0\u2876\u281B\u2801", dot: "", word: "\u2839\u28FF\u28E6\u28C0\u28C0\u28FC\u28FF\u2803 \u28FF\u28FF \u2838\u28FF\u28E6\u28C0\u28E0\u28FE\u28FF\u2847 \u28BF\u28F7\u28C0\u28C0\u28E0\u28F6\u2806 \u28BF\u28FF\u287F \u2818\u28FF\u28FF\u284F \u28BB\u28F7\u28C4\u28C0\u28E0\u28F6\u2806 \u28B8\u28FF \u28FF\u28FF" },
4677
- { strokes: "\u28A4\u28E4\u2836\u283E\u281B\u2801", dot: "", word: " \u2808\u2819\u281B\u281B\u2809 \u2809\u2809 \u2808\u2819\u281B\u280B\u2808\u2809\u2801 \u2809\u281B\u281B\u280B\u2801 \u2808\u2809\u2801 \u2809\u2809 \u2809\u281B\u281B\u280B\u2801 \u2809\u2809 \u2809\u2809" }
4678
- ];
4679
- var BANNER_ICON_COLS = 18;
4680
- var BANNER_GAP = 2;
4681
- var BANNER_COLS = Math.max(
4682
- ...BANNER_ROWS.map((row) => BANNER_ICON_COLS + BANNER_GAP + width(row.word))
4683
- );
4684
- var BANNER_MIN_ROWS = 24;
4685
- var BANNER_WELL_COL = 30;
4686
- function colorWell(word) {
4687
- const cells = [...word];
4688
- return cells.slice(0, BANNER_WELL_COL).join("") + style.cyan(cells.slice(BANNER_WELL_COL).join(""));
5075
+ function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
5076
+ return step(state, [{
5077
+ type: "updateTask",
5078
+ sessionId,
5079
+ taskId: task.id,
5080
+ changes: { assignedRunner: runner },
5081
+ message: `Task #${task.order} runner set to ${runnerLabel}.`
5082
+ }]);
4689
5083
  }
4690
- function renderBanner(rows, cols) {
4691
- if (cols < BANNER_COLS || rows < BANNER_MIN_ROWS) {
4692
- const left2 = " ".repeat(Math.max(0, Math.floor((cols - width(LOCKUP)) / 2)));
4693
- return [left2 + LOCKUP];
4694
- }
4695
- const left = " ".repeat(Math.floor((cols - BANNER_COLS) / 2));
4696
- return BANNER_ROWS.map((row) => {
4697
- const gap = " ".repeat(BANNER_ICON_COLS + BANNER_GAP - width(row.strokes) - width(row.dot));
4698
- return left + row.strokes + (row.dot ? style.cyan(row.dot) : "") + gap + colorWell(row.word);
4699
- });
5084
+ function assignTaskMode(state, sessionId, task, mode, modeLabel) {
5085
+ return step(state, [{
5086
+ type: "updateTask",
5087
+ sessionId,
5088
+ taskId: task.id,
5089
+ changes: { taskMode: mode },
5090
+ message: `Task #${task.order} mode set to ${modeLabel}.`
5091
+ }]);
4700
5092
  }
4701
- function renderWelcome(state, cols) {
4702
- const planner = plannerSetup(state);
4703
- const enabledRunners = state.runners.filter((r) => r.enabled).map((r) => r.name);
4704
- const canPlan = planner.ready || state.runners.length > 0 || state.configuredProviders.length > 0;
4705
- const plannerValue = planner.ready ? [planner.label, state.orchestratorModel || style.yellow("no model")].join(style.grey(" \xB7 ")) : style.yellow(planner.label === "not set" ? "not set" : `${planner.label} (unavailable)`);
4706
- const lines = [
4707
- ...renderBanner(state.rows, cols),
4708
- "",
4709
- ...wrap(style.bold("Describe a goal to start planning."), Math.max(1, cols - 1)),
4710
- ...wrap(
4711
- style.grey("Ordewell researches your codebase, drafts a task plan, then runs each task in a coding agent."),
4712
- Math.max(1, cols - 1)
4713
- ),
4714
- "",
4715
- style.bold("Setup"),
4716
- setupRow("Planner", plannerValue, "/planner \xB7 /model", cols),
4717
- setupRow(
4718
- "Runners",
4719
- enabledRunners.length > 0 ? enabledRunners.join(", ") : style.yellow("none enabled"),
4720
- "/runners",
4721
- cols
4722
- ),
4723
- ""
4724
- ];
4725
- if (!canPlan) {
4726
- lines.push(
4727
- ...wrap(
4728
- style.yellow("Ordewell needs one coding agent installed (Claude Code, Codex, OpenCode) \u2014 or an API key from a provider such as OpenRouter, added with /key."),
4729
- Math.max(1, cols - 1)
4730
- ),
4731
- ""
4732
- );
4733
- }
4734
- lines.push(style.grey("/help all commands"), style.grey(state.workspace));
4735
- return lines;
5093
+ function assignTaskEffort(state, sessionId, task, thinkingEffort) {
5094
+ const assignedModel = task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0;
5095
+ return step(state, [{
5096
+ type: "updateTask",
5097
+ sessionId,
5098
+ taskId: task.id,
5099
+ changes: { assignedModel, thinkingEffort: thinkingEffort ?? null },
5100
+ message: `Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`
5101
+ }]);
4736
5102
  }
4737
- var STATUS_MARK = {
4738
- completed: style.green,
4739
- running: style.yellow,
4740
- in_progress: style.yellow,
4741
- failed: style.red,
4742
- blocked: style.red,
4743
- awaiting_user: style.yellow,
4744
- cancelled: style.grey,
4745
- skipped: style.grey
4746
- };
4747
- var STATUS_ICON = {
4748
- completed: "\u2713",
4749
- failed: "\u2717",
4750
- blocked: "!",
4751
- awaiting_user: "?",
4752
- cancelled: "\u2212",
4753
- skipped: "\u2212",
4754
- approved: "\xB7",
4755
- pending: "\xB7"
4756
- };
4757
- function renderPlan(state, rows, cols) {
4758
- const done = state.tasks.filter((t) => t.status === "completed").length;
4759
- const header = style.bold(`Plan ${done}/${state.tasks.length}`);
4760
- const lines = [header, ""];
4761
- let selectedLine = 2;
4762
- let editorLine;
4763
- state.tasks.forEach((task, i) => {
4764
- const taskStart = lines.length;
4765
- if (i === state.selectedTask) selectedLine = taskStart;
4766
- const renderedTask = renderTask(state, task, i, cols);
4767
- if (task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
4768
- editorLine = taskStart + renderedTask.editorLine;
5103
+ function taskModelCommand(state, args) {
5104
+ return taskCommand(state, args[0], (sessionId, taskId) => {
5105
+ const task = state.tasks.find((candidate) => candidate.id === taskId);
5106
+ if (!args[1]) return openTaskModelPicker(state, task);
5107
+ const model = state.models.find((candidate) => candidate.id === args[1]) ?? {
5108
+ id: args[1],
5109
+ label: args[1],
5110
+ provider: "",
5111
+ variants: []
5112
+ };
5113
+ if (!runnerAccepts(task, model)) {
5114
+ return fail2(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
4769
5115
  }
4770
- lines.push(...renderedTask.lines);
5116
+ return assignTaskModel(state, sessionId, task, model);
4771
5117
  });
4772
- const overflow = Math.max(0, lines.length - rows);
4773
- const anchorLine = editorLine ?? selectedLine;
4774
- const scroll = Math.min(overflow, Math.max(0, anchorLine - rows + 3));
4775
- const totalScroll = Math.min(overflow, scroll + state.planScroll);
4776
- return fit([lines[0], ...lines.slice(1 + totalScroll)], rows, "top");
4777
5118
  }
4778
- function renderTask(state, task, index, cols) {
4779
- const selected = state.focus === "plan" && index === state.selectedTask;
4780
- const expanded = state.expandedTaskId === task.id;
4781
- const running = isTaskRunning(task);
4782
- const paint = STATUS_MARK[task.status] ?? ((t) => t);
4783
- const rawIcon = running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
4784
- const icon = paint(rawIcon);
4785
- const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
4786
- const caret = selected ? style.cyan("\u276F") : " ";
4787
- const head = `${caret} ${icon} ${String(task.order).padStart(2)} ${kind} `;
4788
- const titleRoom = Math.max(1, cols - width(head));
4789
- const wrappedTitle = wrap(task.title, titleRoom);
4790
- const shownTitle = expanded ? wrappedTitle : wrappedTitle.slice(0, 2);
4791
- if (!expanded && wrappedTitle.length > shownTitle.length) {
4792
- const last = shownTitle.length - 1;
4793
- shownTitle[last] = truncate(`${shownTitle[last]}\u2026`, titleRoom);
4794
- }
4795
- const continuation = " ".repeat(width(head));
4796
- const lines = shownTitle.map((line, lineIndex) => {
4797
- const prefix = lineIndex === 0 ? head : continuation;
4798
- return prefix + (selected ? style.bold(line) : line);
5119
+ function taskRunnerCommand(state, args) {
5120
+ return taskCommand(state, args[0], (sessionId, taskId) => {
5121
+ const task = state.tasks.find((candidate) => candidate.id === taskId);
5122
+ if (!args[1]) return openTaskRunnerPicker(state, task);
5123
+ const runner = state.runners.find((candidate) => candidate.id === args[1]);
5124
+ if (!runner) return fail2(state, `Unknown runner "${args[1]}".`);
5125
+ return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
4799
5126
  });
4800
- const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
4801
- const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
4802
- const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}` : "";
4803
- const runner = task.assignedRunner ?? "";
4804
- const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
4805
- if (meta) lines.push(style.grey(truncate(` ${meta}`, cols)));
4806
- if (effort || mode) lines.push(style.grey(truncate(` ${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4807
- let editorLine;
4808
- if (expanded) {
4809
- lines.push(style.grey(` ${task.status.replace(/_/g, " ")}`));
4810
- const editableSeed = task.prompt ?? task.description ?? task.title;
4811
- if (task.description && task.description !== task.title && task.description !== editableSeed) {
4812
- lines.push(...taskText("Description", task.description, cols));
4813
- }
4814
- if (state.taskEditor) {
4815
- const room = paneTextRoom(cols);
4816
- const wrapped = wrapLines(state.taskEditor.text, room);
4817
- const cp = cursorInLines(wrapped, state.taskEditor.cursor, state.taskEditor.text.length);
4818
- editorLine = lines.length + 1 + cp.line;
4819
- lines.push(...renderTaskPrompt(wrapped, cp));
4820
- }
4821
- if (task.dependencies.length > 0) {
4822
- const orderById = new Map(state.tasks.map((candidate) => [candidate.id, candidate.order]));
4823
- const dependencies = task.dependencies.map((id) => orderById.has(id) ? `#${orderById.get(id)}` : id).join(", ");
4824
- lines.push(...taskText("Depends on", dependencies, cols));
4825
- }
4826
- lines.push(style.grey(" enter save \xB7 esc cancel"));
4827
- lines.push(style.grey(truncate(" then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps", cols)));
4828
- }
4829
- return { lines, editorLine };
4830
5127
  }
4831
- function taskText(label, text, cols) {
4832
- const room = paneTextRoom(cols);
4833
- return [
4834
- style.grey(` ${label}`),
4835
- ...wrap(text, room).map((line) => ` ${line}`)
4836
- ];
5128
+ function taskModeCommand(state, args) {
5129
+ return taskCommand(state, args[0], (sessionId, taskId) => {
5130
+ const task = state.tasks.find((candidate) => candidate.id === taskId);
5131
+ if (!args[1]) return openTaskModePicker(state, task);
5132
+ const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
5133
+ if (!mode) return fail2(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
5134
+ return assignTaskMode(state, sessionId, task, mode.id, mode.label);
5135
+ });
4837
5136
  }
4838
- function renderTaskPrompt(wrapped, cursor) {
4839
- return [
4840
- style.grey(" Prompt"),
4841
- ...wrapped.map(({ line }, i) => {
4842
- if (i !== cursor.line) return ` ${line}`;
4843
- const col = cursor.col;
4844
- return ` ${line.slice(0, col)}${style.inverse(line.slice(col, col + 1) || " ")}${line.slice(col + 1)}`;
4845
- })
4846
- ];
5137
+ function taskEffortCommand(state, args) {
5138
+ return taskCommand(state, args[0], (sessionId, taskId) => {
5139
+ const task = state.tasks.find((candidate) => candidate.id === taskId);
5140
+ if (!args[1]) return openTaskEffortPicker(state, task);
5141
+ if (!task.assignedModel) return fail2(state, "Choose a model for this task before setting its thinking effort.");
5142
+ const effort = args[1].toLowerCase();
5143
+ const value = effort === "default" || effort === "none" ? void 0 : args[1];
5144
+ const supported = effortsForTask(state.models, task);
5145
+ if (value && supported.length > 0 && !supported.some((variant) => variant.id === value)) {
5146
+ return fail2(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
5147
+ }
5148
+ return assignTaskEffort(state, sessionId, task, value);
5149
+ });
4847
5150
  }
4848
- function renderOverlay(state, rows, cols) {
4849
- const overlay = state.overlay;
4850
- if (overlay.kind === "help") return renderHelp(overlay.scroll ?? 0, rows, cols);
4851
- if (overlay.kind === "approval") {
4852
- const { request } = overlay;
4853
- const headline = request.kind === "shell_command" ? "The planner wants to run a command" : request.kind === "url_fetch" ? "The planner wants to fetch a URL" : "The planner wants to read a path outside the workspace";
4854
- const queued = state.pendingApprovals.length;
4855
- return frame(
4856
- "Approval needed",
4857
- [
4858
- headline,
4859
- "",
4860
- ` ${style.yellow(request.subject)}`,
4861
- "",
4862
- // Disambiguates the `external_path` case: an auto-tier command like
4863
- // `cat` touching an outside path reads identically to a plain
4864
- // `read_file` without it.
4865
- ...request.detail ? [style.grey(request.detail), ""] : [],
4866
- // The grant is wider than the request, so name it rather than letting
4867
- // the user discover it when the next call silently does not prompt.
4868
- style.grey(`Approving also allows ${request.scope} for the rest of this session.`),
4869
- ...queued > 0 ? [style.grey(`${queued} more waiting.`)] : [],
4870
- "",
4871
- style.grey("y or enter allows \xB7 n or esc denies")
4872
- ],
4873
- rows,
4874
- cols
4875
- );
4876
- }
4877
- if (overlay.kind === "confirm") {
4878
- return frame(
4879
- overlay.title,
4880
- [overlay.message, "", style.grey("enter confirms \xB7 esc cancels")],
4881
- rows,
4882
- cols
4883
- );
4884
- }
4885
- if (overlay.kind === "prompt") {
4886
- const secret = overlay.action.kind === "api-key";
4887
- const shown = secret ? "\u2022".repeat(overlay.value.length) : overlay.value;
4888
- return frame(
4889
- overlay.title,
4890
- [overlay.hint ? style.grey(overlay.hint) : "", "", `${style.cyan("\u276F")} ${shown}`, "", style.grey("enter confirms \xB7 esc cancels")],
4891
- rows,
4892
- cols
4893
- );
4894
- }
4895
- return renderPicker(overlay.picker, rows, cols);
5151
+ function addTask(state, title) {
5152
+ return withSession(state, (sessionId) => {
5153
+ if (!title) {
5154
+ return step({
5155
+ ...state,
5156
+ overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
5157
+ });
5158
+ }
5159
+ return step(state, [{ type: "addTask", sessionId, title }]);
5160
+ });
4896
5161
  }
4897
- function renderPicker(picker2, rows, cols) {
4898
- const items = visibleItems(picker2);
4899
- const body = [];
4900
- if (picker2.hint) body.push(style.grey(picker2.hint));
4901
- body.push(`${style.grey("filter:")} ${picker2.filter}${style.grey("\u258F")}`, "");
4902
- if (items.length === 0) {
4903
- body.push(style.grey(picker2.filter ? "Nothing matches that filter." : "Nothing to show yet\u2026"));
4904
- }
4905
- const room = Math.max(1, rows - body.length - 3);
4906
- const start = Math.max(0, Math.min(picker2.index - Math.floor(room / 2), items.length - room));
4907
- for (const [offset, item] of items.slice(start, start + room).entries()) {
4908
- const active = start + offset === picker2.index;
4909
- const mark = picker2.multi ? picker2.chosen.includes(item.id) ? style.green("[\u2713]") : style.grey("[ ]") : item.selected ? style.green("\u25CF") : " ";
4910
- const caret = active ? style.cyan("\u276F") : " ";
4911
- const label = truncate(item.label, Math.max(1, cols - 12));
4912
- const detail = item.detail ? style.grey(` \u2014 ${item.detail}`) : "";
4913
- const line = item.disabled ? `${caret} ${mark} ${style.grey(label)}${detail}` : `${caret} ${mark} ${active ? style.bold(label) : label}${detail}`;
4914
- body.push(truncate(line, cols - 2));
4915
- }
4916
- body.push("", style.grey(picker2.multi ? "space toggles \xB7 enter confirms \xB7 esc cancels \xB7 type to filter" : "enter selects \xB7 esc cancels \xB7 type to filter"));
4917
- return frame(picker2.title, body, rows, cols);
5162
+ function taskCommand(state, token, run) {
5163
+ return withSession(state, (sessionId) => {
5164
+ if (!token) return fail2(state, "Which task? Pass a task id or its number in the plan.");
5165
+ const taskId = resolveTaskId2(state, token);
5166
+ if (!taskId) return fail2(state, `No task matching "${token}" in the current plan.`);
5167
+ return run(sessionId, taskId);
5168
+ });
4918
5169
  }
4919
- var CATEGORY_TITLE = {
4920
- planning: "Planning",
4921
- tasks: "Tasks",
4922
- models: "Models & providers",
4923
- skills: "Skills",
4924
- session: "Sessions",
4925
- system: "System"
4926
- };
4927
- function renderHelp(scroll, rows, cols) {
4928
- const body = [];
4929
- for (const [category, title] of Object.entries(CATEGORY_TITLE)) {
4930
- const commands = SLASH_COMMANDS.filter((c) => c.category === category);
4931
- if (commands.length === 0) continue;
4932
- body.push(style.bold(title));
4933
- for (const command of commands) {
4934
- body.push(` ${pad(command.usage, Math.min(38, cols - 6))} ${style.grey(command.description)}`);
4935
- }
4936
- body.push("");
5170
+ function resolveTaskId2(state, token) {
5171
+ const byId = state.tasks.find((t) => t.id === token);
5172
+ if (byId) return byId.id;
5173
+ const order = Number(token);
5174
+ if (Number.isInteger(order)) {
5175
+ const byOrder = state.tasks.find((t) => t.order === order);
5176
+ if (byOrder) return byOrder.id;
4937
5177
  }
4938
- body.push(
4939
- style.grey("tab switches panes \xB7 \u2191\u2193/pgup/pgdn scroll \xB7 ctrl-l clears \xB7 ctrl-c quits")
4940
- );
4941
- const wrapped = body.map((line) => truncate(line, Math.max(1, cols - 2)));
4942
- const room = Math.max(1, rows - 2);
4943
- const max = Math.max(0, wrapped.length - room);
4944
- const offset = Math.min(scroll, max);
4945
- const footer = style.grey(offset < max ? "\u2191\u2193 scroll \xB7 any other key closes" : "any key closes");
4946
- return frame("Commands", [...wrapped.slice(offset, offset + room), footer], rows, cols);
4947
- }
4948
- function frame(title, body, rows, cols) {
4949
- const inner = Math.max(1, cols - 2);
4950
- const lines = [
4951
- style.grey(`\u250C\u2500 ${style.bold(title)} ${"\u2500".repeat(Math.max(0, inner - width(title) - 3))}`),
4952
- ...body.flatMap((line) => wrap(line, inner)).map((line) => ` ${truncate(line, inner)}`)
4953
- ];
4954
- return fit(lines, rows, "top");
5178
+ return null;
4955
5179
  }
4956
5180
 
4957
5181
  // src/tui/app.ts
@@ -5004,6 +5228,7 @@ function createApp(deps) {
5004
5228
  }
5005
5229
 
5006
5230
  // src/tui/effects.ts
5231
+ var import_child_process4 = require("child_process");
5007
5232
  var import_core9 = require("@ordewell/core");
5008
5233
  var import_plan_utils4 = require("@ordewell/core/plan-utils");
5009
5234
  var ConversationQueue = class {
@@ -5077,6 +5302,16 @@ function explain(message) {
5077
5302
  function persistAfterDaemon(deps, env) {
5078
5303
  for (const [key, value] of Object.entries(env)) deps.setEnvVar(key, value);
5079
5304
  }
5305
+ function plannerModelNotice(recall2) {
5306
+ switch (recall2.source) {
5307
+ case "remembered":
5308
+ return `Restored ${recall2.model}.`;
5309
+ case "catalog-default":
5310
+ return `Using its default model, ${recall2.model} \u2014 pick another with /model.`;
5311
+ case "none":
5312
+ return "Pick a model with /model.";
5313
+ }
5314
+ }
5080
5315
  async function perform(effect, deps) {
5081
5316
  const { api, dispatch, workspace } = deps;
5082
5317
  switch (effect.type) {
@@ -5101,6 +5336,11 @@ async function perform(effect, deps) {
5101
5336
  await api.stopExecution(effect.sessionId);
5102
5337
  dispatch({ type: "notice", message: "Execution stopped." });
5103
5338
  return;
5339
+ case "cancelPlanning": {
5340
+ const { cancelled } = await api.cancelPlanning(effect.sessionId);
5341
+ if (cancelled) dispatch({ type: "notice", message: "Planning stopped." });
5342
+ return;
5343
+ }
5104
5344
  case "processQueued":
5105
5345
  await api.processQueued(effect.sessionId);
5106
5346
  await refreshPlan(deps, effect.sessionId);
@@ -5159,16 +5399,16 @@ async function perform(effect, deps) {
5159
5399
  const meta = import_core9.ALL_PROVIDERS[effect.provider];
5160
5400
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
5161
5401
  const env = { AI_PROVIDER: effect.provider };
5162
- if (effect.clearModel) {
5163
- env.ORCHESTRATOR_MODEL = "";
5164
- env.ORDEWELL_PLANNER_EFFORT = "";
5165
- }
5166
- await api.updateSettings({ env });
5167
- persistAfterDaemon(deps, env);
5168
- dispatch({ type: "settingsLoaded", settings: { aiProvider: effect.provider, ...effect.clearModel ? { orchestratorModel: "", plannerThinkingEffort: "" } : {} } });
5402
+ const settings = await api.updateSettings({ env });
5403
+ const recall2 = describePlannerSwitch(settings, effect.provider);
5404
+ persistAfterDaemon(deps, { ...env, ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
5405
+ dispatch({
5406
+ type: "settingsLoaded",
5407
+ settings: { aiProvider: effect.provider, orchestratorModel: recall2.model, plannerThinkingEffort: recall2.effort }
5408
+ });
5169
5409
  dispatch({
5170
5410
  type: "notice",
5171
- message: (0, import_core9.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.${effect.clearModel ? ` Using its default model; pick another with /model.` : ""}` : `Planner set to ${meta.label}.${effect.clearModel ? " Pick a model with /model." : ""}`
5411
+ message: `${(0, import_core9.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
5172
5412
  });
5173
5413
  await loadModels(deps);
5174
5414
  return;
@@ -5219,6 +5459,9 @@ async function perform(effect, deps) {
5219
5459
  deps.setMouseCapture(effect.enabled);
5220
5460
  deps.setEnvVar("ORDEWELL_TUI_MOUSE", String(effect.enabled));
5221
5461
  return;
5462
+ case "copyText":
5463
+ copySelection(deps, effect.text);
5464
+ return;
5222
5465
  case "loadModels":
5223
5466
  await loadModels(deps);
5224
5467
  return;
@@ -5256,6 +5499,30 @@ async function perform(effect, deps) {
5256
5499
  return;
5257
5500
  }
5258
5501
  }
5502
+ var pipeToClipboardDefault = (command, text) => {
5503
+ (0, import_child_process4.execSync)(command, { input: text, stdio: ["pipe", "ignore", "ignore"] });
5504
+ };
5505
+ function osc52(text) {
5506
+ return `\x1B]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
5507
+ }
5508
+ function copySelection(deps, text) {
5509
+ if (!text) return;
5510
+ const lines = text.split("\n").length;
5511
+ const command = (0, import_core9.clipboardCopyCommand)(deps.hasBin);
5512
+ if (command) {
5513
+ try {
5514
+ (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
5515
+ deps.dispatch({ type: "notice", message: `Copied ${lines} ${lines === 1 ? "line" : "lines"}.` });
5516
+ return;
5517
+ } catch {
5518
+ }
5519
+ }
5520
+ deps.writeTerminal(osc52(text));
5521
+ deps.dispatch({
5522
+ type: "notice",
5523
+ message: `Copied ${lines} ${lines === 1 ? "line" : "lines"} through the terminal \u2014 no clipboard tool here (pbcopy, xclip, xsel or wl-copy), so it only lands if your terminal supports OSC 52.`
5524
+ });
5525
+ }
5259
5526
  async function withExecutionStream(deps, sessionId, request) {
5260
5527
  let settleReady = () => {
5261
5528
  };
@@ -5490,37 +5757,72 @@ var ESCAPES = {
5490
5757
  "\x1B\r": "alt-enter",
5491
5758
  "\x1B\n": "alt-enter"
5492
5759
  };
5493
- var MOUSE_WHEEL_UP = 64;
5494
- var MOUSE_WHEEL_DOWN = 65;
5495
- var SGR_MOUSE = /^\x1b\[<(\d+);\d+;\d+[Mm]$/;
5760
+ var WHEEL_FLAG = 64;
5761
+ var BUTTON_MASK = 3;
5762
+ var MOTION_FLAG = 32;
5763
+ var MOUSE_MODIFIERS = 4 | 8 | 16 | MOTION_FLAG;
5764
+ var LEFT_BUTTON = 0;
5765
+ var X10_RELEASE = 3;
5766
+ function mouseKey(button, col, row, released = false) {
5767
+ if ((button & WHEEL_FLAG) !== 0) return wheelKey(button, col, row);
5768
+ if (released || (button & BUTTON_MASK) === X10_RELEASE) return { name: "mouseup", col, row };
5769
+ if ((button & BUTTON_MASK) !== LEFT_BUTTON) return { name: "unknown" };
5770
+ return { name: (button & MOTION_FLAG) !== 0 ? "mousedrag" : "mousedown", col, row };
5771
+ }
5772
+ function wheelKey(button, col, row) {
5773
+ if ((button & WHEEL_FLAG) === 0) return { name: "unknown" };
5774
+ switch (button & ~MOUSE_MODIFIERS & (WHEEL_FLAG | BUTTON_MASK)) {
5775
+ case WHEEL_FLAG:
5776
+ return { name: "scrollup", col, row };
5777
+ case WHEEL_FLAG | 1:
5778
+ return { name: "scrolldown", col, row };
5779
+ // 66/67 are the horizontal wheel. Nothing here scrolls sideways, so they
5780
+ // are dropped on purpose rather than falling through to `unknown`, which
5781
+ // the overlays treat as "some key" and would use to close themselves.
5782
+ default:
5783
+ return { name: "wheelignored" };
5784
+ }
5785
+ }
5786
+ var SGR_MOUSE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
5787
+ var URXVT_MOUSE = /^\x1b\[(\d+);(\d+);(\d+)M$/;
5788
+ var X10_PREFIX = "\x1B[M";
5789
+ var X10_BIAS = 32;
5496
5790
  function decodeMouse(seq) {
5497
- const match = SGR_MOUSE.exec(seq);
5498
- if (!match) return void 0;
5499
- const button = Number(match[1]);
5500
- if (button === MOUSE_WHEEL_UP) return { name: "scrollup" };
5501
- if (button === MOUSE_WHEEL_DOWN) return { name: "scrolldown" };
5502
- return { name: "unknown" };
5791
+ const sgr = SGR_MOUSE.exec(seq);
5792
+ if (sgr) return mouseKey(Number(sgr[1]), Number(sgr[2]), Number(sgr[3]), sgr[4] === "m");
5793
+ const urxvt = URXVT_MOUSE.exec(seq);
5794
+ if (urxvt) return mouseKey(Number(urxvt[1]) - X10_BIAS, Number(urxvt[2]), Number(urxvt[3]));
5795
+ if (seq.startsWith(X10_PREFIX) && seq.length === X10_PREFIX.length + 3) {
5796
+ const bytes = [0, 1, 2].map((n) => seq.charCodeAt(X10_PREFIX.length + n) - X10_BIAS);
5797
+ return mouseKey(bytes[0], bytes[1], bytes[2]);
5798
+ }
5799
+ return void 0;
5503
5800
  }
5504
5801
  function decodeKey(seq) {
5505
5802
  const escaped = ESCAPES[seq];
5506
5803
  if (escaped) return { name: escaped };
5507
5804
  const control = CONTROL[seq];
5508
5805
  if (control) return { name: control };
5509
- if (seq.startsWith("\x1B[<")) return decodeMouse(seq) ?? { name: "unknown" };
5806
+ if (seq.startsWith("\x1B[")) {
5807
+ const mouse = decodeMouse(seq);
5808
+ if (mouse) return mouse;
5809
+ }
5510
5810
  if (seq.startsWith("\x1B")) return { name: "unknown" };
5511
5811
  return { name: "char", char: seq };
5512
5812
  }
5513
5813
  var PASTE_START = "\x1B[200~";
5514
5814
  var PASTE_END = "\x1B[201~";
5515
5815
  function pasteKey(raw) {
5516
- const text = raw.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
5816
+ const text = stripTabs(raw.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""));
5517
5817
  return { name: "paste", text };
5518
5818
  }
5519
5819
  function createKeyDecoder() {
5520
5820
  let paste = null;
5821
+ let pending = "";
5521
5822
  return (chunk) => {
5522
5823
  const keys = [];
5523
- let rest = chunk;
5824
+ let rest = pending + chunk;
5825
+ pending = "";
5524
5826
  while (rest.length > 0) {
5525
5827
  if (paste !== null) {
5526
5828
  const combined = paste + rest;
@@ -5536,7 +5838,9 @@ function createKeyDecoder() {
5536
5838
  }
5537
5839
  const start = rest.indexOf(PASTE_START);
5538
5840
  if (start === -1) {
5539
- keys.push(...splitKeys(rest));
5841
+ const split = splitPending(rest);
5842
+ keys.push(...split.keys);
5843
+ pending = split.pending;
5540
5844
  return keys;
5541
5845
  }
5542
5846
  if (start > 0) keys.push(...splitKeys(rest.slice(0, start)));
@@ -5546,7 +5850,32 @@ function createKeyDecoder() {
5546
5850
  return keys;
5547
5851
  };
5548
5852
  }
5549
- function splitKeys(chunk) {
5853
+ var TWO_BYTE_META = /* @__PURE__ */ new Set(["b", "d", "f", "\x7F", "\b", "\r", "\n"]);
5854
+ var CSI_BODY = /[\x20-\x3f]/;
5855
+ var CSI_FINAL = /[@-~]/;
5856
+ var MAX_PENDING = 32;
5857
+ var INCOMPLETE = -1;
5858
+ function csiEnd(chars, start, from) {
5859
+ let end = from;
5860
+ while (end < chars.length && !CSI_FINAL.test(chars[end])) {
5861
+ if (!CSI_BODY.test(chars[end])) return end;
5862
+ end += 1;
5863
+ }
5864
+ if (end >= chars.length) return end - start >= MAX_PENDING ? end : INCOMPLETE;
5865
+ return end + 1;
5866
+ }
5867
+ function sequenceEnd(chars, i) {
5868
+ const next = chars[i + 1];
5869
+ if (next === void 0) return i + 1;
5870
+ if (next === "[" && chars[i + 2] === "M") {
5871
+ return i + X10_PREFIX.length + 3 <= chars.length ? i + X10_PREFIX.length + 3 : INCOMPLETE;
5872
+ }
5873
+ if (next === "[" || next === "O") return csiEnd(chars, i, i + 2);
5874
+ if (TWO_BYTE_META.has(next)) return i + 2;
5875
+ if (next === "\x1B" && (chars[i + 2] === "[" || chars[i + 2] === "O")) return csiEnd(chars, i, i + 3);
5876
+ return i + 1;
5877
+ }
5878
+ function splitPending(chunk) {
5550
5879
  const keys = [];
5551
5880
  const chars = [...chunk];
5552
5881
  let i = 0;
@@ -5556,21 +5885,16 @@ function splitKeys(chunk) {
5556
5885
  i += 1;
5557
5886
  continue;
5558
5887
  }
5559
- let end = i + 1;
5560
- if (chars[end] === "[" || chars[end] === "O") {
5561
- end += 1;
5562
- while (end < chars.length && !/[@-~]/.test(chars[end])) end += 1;
5563
- end += 1;
5564
- } else if (chars[end] === "b" || chars[end] === "d" || chars[end] === "f" || chars[end] === "\x7F" || chars[end] === "\b" || chars[end] === "\r" || chars[end] === "\n") {
5565
- end += 1;
5566
- } else if (chars[end] === "\x1B" && (chars[end + 1] === "[" || chars[end + 1] === "O")) {
5567
- end += 2;
5568
- while (end < chars.length && !/[@-~]/.test(chars[end])) end += 1;
5569
- end += 1;
5570
- }
5888
+ const end = sequenceEnd(chars, i);
5889
+ if (end === INCOMPLETE) return { keys, pending: chars.slice(i).join("") };
5571
5890
  keys.push(decodeKey(chars.slice(i, end).join("")));
5572
5891
  i = end;
5573
5892
  }
5893
+ return { keys, pending: "" };
5894
+ }
5895
+ function splitKeys(chunk) {
5896
+ const { keys, pending } = splitPending(chunk);
5897
+ if (pending) keys.push(decodeKey(pending));
5574
5898
  return keys;
5575
5899
  }
5576
5900
 
@@ -5583,13 +5907,17 @@ var PASTE_MODE_ON = "\x1B[?2004h";
5583
5907
  var PASTE_MODE_OFF = "\x1B[?2004l";
5584
5908
  var KITTY_KEYBOARD_ON = "\x1B[>1u";
5585
5909
  var KITTY_KEYBOARD_OFF = "\x1B[<u";
5586
- var MOUSE_TRACKING_ON = "\x1B[?1000h\x1B[?1006h";
5587
- var MOUSE_TRACKING_OFF = "\x1B[?1000l\x1B[?1006l";
5910
+ var MOUSE_TRACKING_ON = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
5911
+ var MOUSE_TRACKING_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1006l";
5588
5912
  var ALT_SCROLL_SAVE = "\x1B[?1007s";
5589
5913
  var ALT_SCROLL_OFF = "\x1B[?1007l";
5590
5914
  var ALT_SCROLL_RESTORE = "\x1B[?1007r";
5591
- var HOME = "\x1B[H";
5592
5915
  var CLEAR = "\x1B[2J";
5916
+ var AUTOWRAP_OFF = "\x1B[?7l";
5917
+ var AUTOWRAP_ON = "\x1B[?7h";
5918
+ function clampToCols(line, cols) {
5919
+ return width(line) > cols ? truncate(line, cols) : line;
5920
+ }
5593
5921
  function openTerminal(options) {
5594
5922
  const input = options.input ?? process.stdin;
5595
5923
  const output = options.output ?? process.stdout;
@@ -5598,15 +5926,14 @@ function openTerminal(options) {
5598
5926
  input.resume();
5599
5927
  input.setEncoding("utf8");
5600
5928
  let mouse = options.mouse === true;
5601
- output.write(
5602
- ALT_SCREEN_ON + HIDE_CURSOR + PASTE_MODE_ON + KITTY_KEYBOARD_ON + ALT_SCROLL_SAVE + ALT_SCROLL_OFF + (mouse ? MOUSE_TRACKING_ON : "") + CLEAR
5603
- );
5929
+ const modes = () => ALT_SCREEN_ON + HIDE_CURSOR + PASTE_MODE_ON + KITTY_KEYBOARD_ON + ALT_SCROLL_SAVE + ALT_SCROLL_OFF + (mouse ? MOUSE_TRACKING_ON : "");
5930
+ output.write(modes() + CLEAR);
5604
5931
  const decode = createKeyDecoder();
5605
5932
  const onData = (chunk) => {
5606
5933
  for (const key of decode(chunk)) options.onKey(key);
5607
5934
  };
5608
5935
  const onResize = () => {
5609
- output.write(CLEAR);
5936
+ output.write(CLEAR + (mouse ? MOUSE_TRACKING_ON : ""));
5610
5937
  const { rows, cols } = size();
5611
5938
  options.onResize(rows, cols);
5612
5939
  };
@@ -5620,13 +5947,19 @@ function openTerminal(options) {
5620
5947
  size,
5621
5948
  draw(frame2) {
5622
5949
  if (closed) return;
5623
- output.write(HOME + frame2.join("\n"));
5950
+ const { cols } = size();
5951
+ const rows = frame2.map((line, i) => `\x1B[${i + 1};1H${clampToCols(line, cols)}\x1B[K`).join("");
5952
+ output.write(AUTOWRAP_OFF + rows + AUTOWRAP_ON);
5624
5953
  },
5625
5954
  setMouse(enabled) {
5626
5955
  if (closed || enabled === mouse) return;
5627
5956
  mouse = enabled;
5628
5957
  output.write(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
5629
5958
  },
5959
+ reset() {
5960
+ if (closed) return;
5961
+ output.write(modes() + CLEAR);
5962
+ },
5630
5963
  close() {
5631
5964
  if (closed) return;
5632
5965
  closed = true;
@@ -5635,7 +5968,7 @@ function openTerminal(options) {
5635
5968
  if (input.isTTY) input.setRawMode(false);
5636
5969
  input.pause();
5637
5970
  output.write(
5638
- MOUSE_TRACKING_OFF + ALT_SCROLL_RESTORE + KITTY_KEYBOARD_OFF + PASTE_MODE_OFF + SHOW_CURSOR + ALT_SCREEN_OFF
5971
+ MOUSE_TRACKING_OFF + ALT_SCROLL_RESTORE + KITTY_KEYBOARD_OFF + PASTE_MODE_OFF + AUTOWRAP_ON + SHOW_CURSOR + ALT_SCREEN_OFF
5639
5972
  );
5640
5973
  }
5641
5974
  };
@@ -5648,13 +5981,23 @@ async function handleTui(subArgs) {
5648
5981
  process.exit(1);
5649
5982
  }
5650
5983
  const workspace = flag(subArgs, "--workspace") || process.cwd();
5984
+ try {
5985
+ (0, import_core10.assertWorkspaceExists)(workspace);
5986
+ } catch {
5987
+ console.error(
5988
+ `Workspace "${workspace}" does not exist.
5989
+
5990
+ cd into a directory that exists, or pass --workspace <path> naming one that does.`
5991
+ );
5992
+ process.exit(1);
5993
+ }
5651
5994
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(resolvePort(subArgs), { detached: false });
5652
5995
  let owned = ownedAtLaunch;
5653
5996
  const api = new ApiClient(port);
5654
5997
  const conversationQueue = new ConversationQueue();
5655
5998
  let terminal;
5656
5999
  const autoMode = process.env.ORDEWELL_AUTONOMOUS_MODE;
5657
- const mouseCapture = process.env.ORDEWELL_TUI_MOUSE === "true" || process.env.ORDEWELL_TUI_MOUSE === "1";
6000
+ const mouseCapture = process.env.ORDEWELL_TUI_MOUSE !== "false" && process.env.ORDEWELL_TUI_MOUSE !== "0";
5658
6001
  const app = createApp({
5659
6002
  initial: { workspace, autonomous: autoMode !== "false" && autoMode !== "0", mouseCapture },
5660
6003
  draw: (frame2) => terminal?.draw(frame2),
@@ -5671,6 +6014,9 @@ async function handleTui(subArgs) {
5671
6014
  },
5672
6015
  openTerminal: (sessionId, taskId) => openTaskTerminal(port, sessionId, taskId),
5673
6016
  setMouseCapture: (enabled) => terminal?.setMouse(enabled),
6017
+ // The alt screen is up with the cursor hidden, so an OSC 52 write shows
6018
+ // nothing — the terminal reads it and answers by taking the clipboard.
6019
+ writeTerminal: (data) => process.stdout.write(data),
5674
6020
  reviveDaemon: async () => {
5675
6021
  const { owned: freshlyOwned } = await ensureDaemonOwned(port, { detached: false, quiet: true });
5676
6022
  if (freshlyOwned) owned = true;
@@ -5697,6 +6043,11 @@ async function handleTui(subArgs) {
5697
6043
  process.on("SIGINT", () => shutdown(0));
5698
6044
  process.on("SIGTERM", () => shutdown(0));
5699
6045
  process.on("SIGHUP", () => shutdown(0));
6046
+ process.on("SIGCONT", () => {
6047
+ terminal?.reset();
6048
+ const { rows: r, cols: c } = terminal?.size() ?? { rows: 24, cols: 80 };
6049
+ app.dispatch({ type: "resize", rows: r, cols: c });
6050
+ });
5700
6051
  process.on("uncaughtException", (err) => {
5701
6052
  console.error(`Fatal: ${err.message}`);
5702
6053
  shutdown(1);