@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.
- package/dist/main.js +1499 -1148
- 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
|
-
|
|
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
|
|
2140
|
-
const
|
|
2141
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2162
|
-
|
|
2163
|
-
|
|
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
|
|
2970
|
-
if (
|
|
2971
|
-
rows.push(
|
|
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
|
|
2978
|
-
return
|
|
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 = (
|
|
2993
|
-
const wrapped =
|
|
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:
|
|
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:
|
|
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/
|
|
3180
|
-
var
|
|
3181
|
-
var
|
|
3182
|
-
function
|
|
3183
|
-
|
|
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
|
|
3186
|
-
const
|
|
3187
|
-
|
|
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
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
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
|
-
|
|
3197
|
-
|
|
3198
|
-
const
|
|
3199
|
-
return
|
|
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
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
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
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
const
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
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
|
-
|
|
3270
|
-
|
|
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
|
|
3275
|
-
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
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,
|
|
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
|
-
|
|
3570
|
-
|
|
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:
|
|
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:
|
|
4353
|
+
planScroll: null
|
|
3615
4354
|
});
|
|
3616
4355
|
}
|
|
3617
|
-
|
|
3618
|
-
|
|
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
|
-
|
|
3657
|
-
|
|
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:
|
|
3730
|
-
pageup: -
|
|
3731
|
-
scrolldown:
|
|
3732
|
-
scrollup: -
|
|
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:
|
|
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"
|
|
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
|
-
|
|
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(
|
|
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
|
|
4223
|
-
|
|
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
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
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
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
return
|
|
4241
|
-
|
|
4242
|
-
|
|
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
|
|
4253
|
-
const
|
|
4254
|
-
|
|
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:
|
|
4260
|
-
message:
|
|
4261
|
-
action: { kind: "
|
|
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
|
|
4266
|
-
const
|
|
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
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
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
|
|
4282
|
-
if (
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
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
|
-
|
|
4301
|
-
|
|
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(
|
|
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
|
|
4334
|
-
const
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
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
|
|
4399
|
-
|
|
4400
|
-
const
|
|
4401
|
-
if (!
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
return
|
|
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
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
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
|
|
4422
|
-
|
|
4423
|
-
|
|
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:
|
|
4860
|
+
overlay: keyPrompt(provider)
|
|
4427
4861
|
});
|
|
4428
4862
|
}
|
|
4429
|
-
return step(state, [{ type: "
|
|
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
|
-
|
|
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
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
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
|
|
4471
|
-
|
|
4472
|
-
|
|
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
|
-
|
|
4475
|
-
|
|
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
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
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
|
-
|
|
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
|
|
4529
|
-
|
|
4530
|
-
|
|
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
|
|
4551
|
-
const
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
const
|
|
4555
|
-
if (
|
|
4556
|
-
|
|
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
|
-
|
|
4563
|
-
|
|
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
|
|
4566
|
-
const
|
|
4567
|
-
if (
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
}
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
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
|
-
|
|
4609
|
-
|
|
4610
|
-
const
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
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
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
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
|
-
|
|
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
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
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
|
|
4644
|
-
|
|
4645
|
-
|
|
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
|
|
4648
|
-
|
|
4649
|
-
const
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
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
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
const
|
|
4659
|
-
|
|
4660
|
-
|
|
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
|
-
|
|
4663
|
-
|
|
4664
|
-
return
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
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
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
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
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
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
|
|
4702
|
-
const
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
""
|
|
4709
|
-
|
|
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
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
};
|
|
4747
|
-
|
|
4748
|
-
|
|
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
|
-
|
|
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
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
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
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
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
|
|
4839
|
-
return [
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
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
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
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
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
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
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
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
|
|
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
|
|
5494
|
-
var
|
|
5495
|
-
var
|
|
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
|
|
5498
|
-
if (
|
|
5499
|
-
const
|
|
5500
|
-
if (
|
|
5501
|
-
if (
|
|
5502
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5560
|
-
if (
|
|
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
|
-
|
|
5602
|
-
|
|
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
|
-
|
|
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
|
|
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);
|