@ordewell/cli 0.5.0 → 0.5.2
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 +411 -157
- package/package.json +4 -4
package/dist/main.js
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/main.ts
|
|
27
|
-
var
|
|
27
|
+
var import_core23 = require("@ordewell/core");
|
|
28
28
|
|
|
29
29
|
// src/utils/env.ts
|
|
30
30
|
var import_fs = require("fs");
|
|
@@ -94,7 +94,8 @@ Planning:
|
|
|
94
94
|
ordewell sessions list|load|delete Manage named sessions
|
|
95
95
|
ordewell fork Continue in a copy of the conversation and its tasks
|
|
96
96
|
ordewell compact Condense the conversation into a summary (tasks are kept)
|
|
97
|
-
ordewell rewind [<n>]
|
|
97
|
+
ordewell rewind [<n>] Fork the conversation from before message <n> (no <n>: list them)
|
|
98
|
+
ordewell handoff [action] Review, merge, discard or clean up a finished isolated run
|
|
98
99
|
|
|
99
100
|
Tasks (<id> is an order number or a task ID):
|
|
100
101
|
ordewell add-task --title "..." Add a task to the current plan
|
|
@@ -123,6 +124,7 @@ Planner, models and runners:
|
|
|
123
124
|
ordewell runners [<id> on|off] Enable or disable runners (claude-code, opencode, codex)
|
|
124
125
|
ordewell allowlist set|clear|show Limit which models a runner may use
|
|
125
126
|
ordewell auto [on|off] Autonomous permission mode for new sessions
|
|
127
|
+
ordewell parallel [<n>] How many AI tasks run at once (default 3; applies to a live run)
|
|
126
128
|
ordewell refresh Re-discover runners and model catalogs
|
|
127
129
|
ordewell models List every provider's catalog (works without a server)
|
|
128
130
|
|
|
@@ -196,8 +198,9 @@ Environment:
|
|
|
196
198
|
OPENROUTER_BASE_URL Default: https://openrouter.ai/api/v1
|
|
197
199
|
ORDEWELL_AUTONOMOUS_MODE Approval posture for new sessions (see "ordewell auto")
|
|
198
200
|
ORDEWELL_PORT Daemon port CLI commands target (default: 3742)
|
|
201
|
+
ORDEWELL_DIRENV false: agents skip the project's direnv .envrc
|
|
199
202
|
ORDEWELL_RESEARCH_ENABLED true (default) or false
|
|
200
|
-
ORDEWELL_MAX_PARALLEL Default: 3
|
|
203
|
+
ORDEWELL_MAX_PARALLEL Default: 3 \u2014 set with "ordewell parallel"
|
|
201
204
|
`);
|
|
202
205
|
}
|
|
203
206
|
|
|
@@ -616,13 +619,17 @@ var ApiClient = class {
|
|
|
616
619
|
}
|
|
617
620
|
return res.data.targets;
|
|
618
621
|
}
|
|
619
|
-
/**
|
|
622
|
+
/**
|
|
623
|
+
* Fork the conversation from just before the user message at `index` (a
|
|
624
|
+
* transcript position) into a session the daemon has already adopted. The
|
|
625
|
+
* original is left as it was; `rewoundMessage` is that message in full.
|
|
626
|
+
*/
|
|
620
627
|
async rewindConversation(sessionId, index) {
|
|
621
628
|
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
|
|
622
629
|
if (res.status !== 200) {
|
|
623
630
|
throw new Error(res.data?.error || "Rewind failed");
|
|
624
631
|
}
|
|
625
|
-
return res.data.plan;
|
|
632
|
+
return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan, rewoundMessage: res.data.rewoundMessage };
|
|
626
633
|
}
|
|
627
634
|
/**
|
|
628
635
|
* Condense the conversation into a summary. One planner call, so it can take
|
|
@@ -937,6 +944,25 @@ function readLastSession(workspace) {
|
|
|
937
944
|
}
|
|
938
945
|
return null;
|
|
939
946
|
}
|
|
947
|
+
var SESSION_PREFIX = "session-";
|
|
948
|
+
function expandSessionId(id, workspace) {
|
|
949
|
+
if (id.startsWith(SESSION_PREFIX)) return id;
|
|
950
|
+
let files;
|
|
951
|
+
try {
|
|
952
|
+
files = (0, import_fs3.readdirSync)((0, import_path3.join)((0, import_core5.getStateDir)(workspace), "sessions")).filter((f) => f.endsWith(".json"));
|
|
953
|
+
} catch {
|
|
954
|
+
return id;
|
|
955
|
+
}
|
|
956
|
+
const matches = /* @__PURE__ */ new Set();
|
|
957
|
+
for (const file of files) {
|
|
958
|
+
try {
|
|
959
|
+
const full = JSON.parse((0, import_fs3.readFileSync)((0, import_path3.join)((0, import_core5.getStateDir)(workspace), "sessions", file), "utf8"))?.meta?.id;
|
|
960
|
+
if (typeof full === "string" && (full.endsWith(id) || full.slice(SESSION_PREFIX.length).startsWith(id))) matches.add(full);
|
|
961
|
+
} catch {
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return matches.size === 1 ? [...matches][0] : id;
|
|
965
|
+
}
|
|
940
966
|
|
|
941
967
|
// src/approvals.ts
|
|
942
968
|
function describeApproval(event) {
|
|
@@ -1262,6 +1288,7 @@ var STATE_OF = {
|
|
|
1262
1288
|
active: "active",
|
|
1263
1289
|
merged: "integrated",
|
|
1264
1290
|
conflict: "conflict",
|
|
1291
|
+
repairing: "repairing",
|
|
1265
1292
|
// A failed integration keeps its refs exactly as a failed verdict does; the
|
|
1266
1293
|
// user sees one thing: work that did not land and can be looked at.
|
|
1267
1294
|
kept: "kept",
|
|
@@ -1293,19 +1320,25 @@ function isolationOfPlan(plan) {
|
|
|
1293
1320
|
const state = STATE_OF[String(record.status)];
|
|
1294
1321
|
if (!state) continue;
|
|
1295
1322
|
const changed = changedRepos(record, legacy, state);
|
|
1323
|
+
const repairedFiles = Array.isArray(record.repairedFiles) && record.repairedFiles.length > 0 ? record.repairedFiles.map(String) : void 0;
|
|
1296
1324
|
tasks[taskId] = {
|
|
1297
1325
|
state,
|
|
1298
1326
|
branch: String(record.branch ?? ""),
|
|
1299
1327
|
worktree: String(record.workspace ?? record.worktree ?? ""),
|
|
1300
1328
|
repos: changed,
|
|
1301
|
-
...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {}
|
|
1329
|
+
...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {},
|
|
1330
|
+
...Array.isArray(record.conflictFiles) && record.conflictFiles.length > 0 ? { conflictFiles: record.conflictFiles.map(String) } : {},
|
|
1331
|
+
// No `repair` (attempt/limit): `conflictRepairAttempts` is server config, never
|
|
1332
|
+
// sent in a persisted plan, so it cannot be reconstructed here. A live
|
|
1333
|
+
// `tasksStatus` update fills it in once the stream catches up.
|
|
1334
|
+
...repairedFiles ? { repairedFiles } : {}
|
|
1302
1335
|
};
|
|
1303
1336
|
if (state === "integrated") {
|
|
1304
|
-
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed });
|
|
1337
|
+
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed, ...repairedFiles ? { repairedFiles } : {} });
|
|
1305
1338
|
}
|
|
1306
1339
|
}
|
|
1307
1340
|
landed.sort((a, b) => a.order - b.order);
|
|
1308
|
-
const entry = ({ taskId, order, title }) => ({ taskId, order, title });
|
|
1341
|
+
const entry = ({ taskId, order, title, repairedFiles }) => ({ taskId, order, title, ...repairedFiles ? { repairedFiles } : {} });
|
|
1309
1342
|
return {
|
|
1310
1343
|
handoff: {
|
|
1311
1344
|
repos: repos.map((repo) => ({ ...repo, landed: landed.filter((t) => t.changed.includes(repo.path)).map(entry) })),
|
|
@@ -1328,6 +1361,16 @@ function reposWithWork(handoff) {
|
|
|
1328
1361
|
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
1329
1362
|
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
1330
1363
|
}
|
|
1364
|
+
function repairedLanded(handoff) {
|
|
1365
|
+
return handoff.landed.filter((t) => t.repairedFiles?.length);
|
|
1366
|
+
}
|
|
1367
|
+
function repairedNotice(handoff) {
|
|
1368
|
+
const repaired = repairedLanded(handoff);
|
|
1369
|
+
if (repaired.length === 0) return "";
|
|
1370
|
+
const named = repaired.map((t) => `${t.title} (${(0, import_core7.capConflictFiles)(t.repairedFiles)})`);
|
|
1371
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
1372
|
+
return ` ${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
1373
|
+
}
|
|
1331
1374
|
function repoResultLines(handoff) {
|
|
1332
1375
|
return handoff.repos.map(({ path, landed }) => {
|
|
1333
1376
|
const n = landed.length;
|
|
@@ -1337,12 +1380,12 @@ function repoResultLines(handoff) {
|
|
|
1337
1380
|
function taskRepoNames(isolation) {
|
|
1338
1381
|
return (isolation?.repos ?? []).filter((r) => r !== ".");
|
|
1339
1382
|
}
|
|
1340
|
-
function mergeOutcome(result, branch, group) {
|
|
1383
|
+
function mergeOutcome(result, branch, group, repaired = []) {
|
|
1341
1384
|
const ok = result.outcome === "merged";
|
|
1342
1385
|
if (!group && result.outcome === "conflict") {
|
|
1343
1386
|
return { ok, message: `Merging ${branch} conflicted, so it was aborted \u2014 your tree is as it was. Merge it with git and resolve the conflict there.` };
|
|
1344
1387
|
}
|
|
1345
|
-
const { message } = (0, import_core7.describeMergeResult)(result, branch, group);
|
|
1388
|
+
const { message } = (0, import_core7.describeMergeResult)(result, branch, group, repaired);
|
|
1346
1389
|
return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
|
|
1347
1390
|
}
|
|
1348
1391
|
|
|
@@ -2384,7 +2427,7 @@ async function handleCompact(subArgs, injectedApi) {
|
|
|
2384
2427
|
async function handleRewind(subArgs, injectedApi) {
|
|
2385
2428
|
const arg = positionals(subArgs)[0];
|
|
2386
2429
|
if (arg !== void 0 && !/^\d+$/.test(arg)) fail(REWIND_USAGE);
|
|
2387
|
-
const { api, sessionId } = await adopted(subArgs, injectedApi);
|
|
2430
|
+
const { api, sessionId, workspace } = await adopted(subArgs, injectedApi);
|
|
2388
2431
|
if (arg === void 0) {
|
|
2389
2432
|
let targets;
|
|
2390
2433
|
try {
|
|
@@ -2398,15 +2441,20 @@ async function handleRewind(subArgs, injectedApi) {
|
|
|
2398
2441
|
}
|
|
2399
2442
|
console.log("Your messages, most recent first:\n");
|
|
2400
2443
|
for (const t of [...targets].reverse()) console.log(` ${String(t.index).padStart(4)} ${t.preview}`);
|
|
2401
|
-
console.log("\n `ordewell rewind <n>`
|
|
2444
|
+
console.log("\n `ordewell rewind <n>` forks the conversation from just before message <n>. The original is kept, and the tasks ride along.");
|
|
2402
2445
|
return;
|
|
2403
2446
|
}
|
|
2447
|
+
let fork;
|
|
2404
2448
|
try {
|
|
2405
|
-
await api.rewindConversation(sessionId, Number(arg));
|
|
2449
|
+
fork = await api.rewindConversation(sessionId, Number(arg));
|
|
2406
2450
|
} catch (err) {
|
|
2407
2451
|
fail(`Failed to rewind: ${err.message}`);
|
|
2408
2452
|
}
|
|
2409
|
-
|
|
2453
|
+
saveLastSession(fork.sessionId, fork.goal, fork.plan.runners ?? [], workspace);
|
|
2454
|
+
console.log(`Forked ${sessionId} into ${fork.sessionId} from just before message ${arg} \u2014 the fork is now the current session.`);
|
|
2455
|
+
console.log(` \`ordewell sessions load ${sessionId}\` to go back to the original.`);
|
|
2456
|
+
console.log("\nThe message you rewound to, to resend or edit:\n");
|
|
2457
|
+
console.log(fork.rewoundMessage);
|
|
2410
2458
|
}
|
|
2411
2459
|
|
|
2412
2460
|
// src/commands/handoff.ts
|
|
@@ -2619,12 +2667,12 @@ function runHandoffAction(state, id) {
|
|
|
2619
2667
|
overlay: isRepoGroup(handoff) ? {
|
|
2620
2668
|
kind: "confirm",
|
|
2621
2669
|
title: "Merge all into your branches?",
|
|
2622
|
-
message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. Ordewell never does this on its own. It checks every repository first and merges none unless every repository can take it; on git older than 2.38 it goes repository by repository and stops at the first that fails. A merge that conflicts is aborted, and your trees stay as they were
|
|
2670
|
+
message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. Ordewell never does this on its own. It checks every repository first and merges none unless every repository can take it; on git older than 2.38 it goes repository by repository and stops at the first that fails. A merge that conflicts is aborted, and your trees stay as they were. Once every repository has merged, the run's worktrees and branches are removed.${repairedNotice(handoff)}`,
|
|
2623
2671
|
action: { kind: "merge-run" }
|
|
2624
2672
|
} : {
|
|
2625
2673
|
kind: "confirm",
|
|
2626
2674
|
title: "Merge into your branch?",
|
|
2627
|
-
message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. Ordewell never does this on its own. If it conflicts the merge is aborted and your tree stays as it was
|
|
2675
|
+
message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. Ordewell never does this on its own. If it conflicts the merge is aborted and your tree stays as it was. Once it has merged, the run's worktrees and branches are removed.${repairedNotice(handoff)}`,
|
|
2628
2676
|
action: { kind: "merge-run" }
|
|
2629
2677
|
}
|
|
2630
2678
|
},
|
|
@@ -2649,7 +2697,14 @@ function confirmedHandoff(state, kind) {
|
|
|
2649
2697
|
const closed = { ...state, overlay: null };
|
|
2650
2698
|
if (!state.sessionId || !state.handoff) return { state: closed, effects: [] };
|
|
2651
2699
|
const { sessionId, handoff } = state;
|
|
2652
|
-
const
|
|
2700
|
+
const repaired = repairedLanded(handoff);
|
|
2701
|
+
const effect = kind === "merge-run" ? {
|
|
2702
|
+
type: "isolationMerge",
|
|
2703
|
+
sessionId,
|
|
2704
|
+
branch: handoffBranch(handoff),
|
|
2705
|
+
...isRepoGroup(handoff) ? { group: true } : {},
|
|
2706
|
+
...repaired.length > 0 ? { repaired } : {}
|
|
2707
|
+
} : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
|
|
2653
2708
|
return { state: closed, effects: [effect] };
|
|
2654
2709
|
}
|
|
2655
2710
|
function showDiff(state, diff) {
|
|
@@ -2714,7 +2769,7 @@ function chooseBlocked(state, choice) {
|
|
|
2714
2769
|
return { state: closed, effects: [{ type: "isolationContinue", sessionId, mode: choice }] };
|
|
2715
2770
|
}
|
|
2716
2771
|
function sameIsolation(a, b) {
|
|
2717
|
-
return a?.state === b?.state && a?.branch === b?.branch && a?.worktree === b?.worktree && a?.conflictRepo === b?.conflictRepo && (a?.repos ?? []).join("\0") === (b?.repos ?? []).join("\0");
|
|
2772
|
+
return a?.state === b?.state && a?.branch === b?.branch && a?.worktree === b?.worktree && a?.conflictRepo === b?.conflictRepo && (a?.repos ?? []).join("\0") === (b?.repos ?? []).join("\0") && (a?.conflictFiles ?? []).join("\0") === (b?.conflictFiles ?? []).join("\0") && a?.repair?.attempt === b?.repair?.attempt && a?.repair?.limit === b?.repair?.limit && (a?.repairedFiles ?? []).join("\0") === (b?.repairedFiles ?? []).join("\0");
|
|
2718
2773
|
}
|
|
2719
2774
|
function mapTasks(tasks, map) {
|
|
2720
2775
|
return tasks.map((task) => {
|
|
@@ -2787,8 +2842,8 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
|
2787
2842
|
return;
|
|
2788
2843
|
}
|
|
2789
2844
|
case "merge": {
|
|
2790
|
-
await confirmed(group ? `Merge ${branch} into the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it.` : `Merge ${branch} into the branch you have checked out
|
|
2791
|
-
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group);
|
|
2845
|
+
await confirmed((group ? `Merge ${branch} into the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it; once all have, the run's worktrees and branches are removed.` : `Merge ${branch} into the branch you have checked out? Once merged, the run's worktrees and branches are removed.`) + repairedNotice(handoff));
|
|
2846
|
+
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
|
|
2792
2847
|
if (ok) console.log(message);
|
|
2793
2848
|
else fail(message);
|
|
2794
2849
|
return;
|
|
@@ -2928,30 +2983,47 @@ async function handlePlannerEffort(subArgs, injectedApi) {
|
|
|
2928
2983
|
console.log(`Planner effort set to ${level}.`);
|
|
2929
2984
|
}
|
|
2930
2985
|
|
|
2931
|
-
// src/commands/
|
|
2986
|
+
// src/commands/parallel.ts
|
|
2932
2987
|
var import_core14 = require("@ordewell/core");
|
|
2933
|
-
var USAGE5 = "Usage: ordewell
|
|
2988
|
+
var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
|
|
2989
|
+
async function handleParallel(subArgs, injectedApi) {
|
|
2990
|
+
const [wanted] = positionals(subArgs);
|
|
2991
|
+
const api = await connect(subArgs, injectedApi);
|
|
2992
|
+
if (!wanted || wanted === "show") {
|
|
2993
|
+
const settings = await api.getSettings();
|
|
2994
|
+
console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
const limit = (0, import_core14.parseMaxParallel)(wanted);
|
|
2998
|
+
if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
|
|
2999
|
+
await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
|
|
3000
|
+
console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// src/commands/key.ts
|
|
3004
|
+
var import_core15 = require("@ordewell/core");
|
|
3005
|
+
var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
|
|
2934
3006
|
async function handleKey(subArgs, injectedApi) {
|
|
2935
3007
|
const [action, provider, ...keyParts] = positionals(subArgs);
|
|
2936
3008
|
if (action && action !== "list" && action !== "set") {
|
|
2937
|
-
fail(`Unknown key action: "${action}"`,
|
|
3009
|
+
fail(`Unknown key action: "${action}"`, USAGE6);
|
|
2938
3010
|
}
|
|
2939
3011
|
const api = await connect(subArgs, injectedApi);
|
|
2940
3012
|
if (action !== "set") {
|
|
2941
3013
|
const catalog = await fetchCatalog(api);
|
|
2942
3014
|
console.log("\nAPI provider keys\n");
|
|
2943
|
-
for (const id2 of
|
|
2944
|
-
const meta2 =
|
|
3015
|
+
for (const id2 of import_core15.PROVIDER_PRIORITY) {
|
|
3016
|
+
const meta2 = import_core15.ALL_PROVIDERS[id2];
|
|
2945
3017
|
const configured = catalog.providers.includes(id2);
|
|
2946
3018
|
console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
|
|
2947
3019
|
}
|
|
2948
3020
|
console.log(`
|
|
2949
|
-
\u2713 = a working key was found. ${
|
|
3021
|
+
\u2713 = a working key was found. ${USAGE6}`);
|
|
2950
3022
|
return;
|
|
2951
3023
|
}
|
|
2952
|
-
if (!provider) fail(
|
|
3024
|
+
if (!provider) fail(USAGE6);
|
|
2953
3025
|
const id = provider.toLowerCase();
|
|
2954
|
-
const meta =
|
|
3026
|
+
const meta = import_core15.ALL_PROVIDERS[id];
|
|
2955
3027
|
if (!meta) {
|
|
2956
3028
|
fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
|
|
2957
3029
|
}
|
|
@@ -2962,7 +3034,7 @@ async function handleKey(subArgs, injectedApi) {
|
|
|
2962
3034
|
}
|
|
2963
3035
|
|
|
2964
3036
|
// src/commands/runners.ts
|
|
2965
|
-
var
|
|
3037
|
+
var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
|
|
2966
3038
|
async function handleRunners(subArgs, injectedApi) {
|
|
2967
3039
|
const [runner, toggle] = positionals(subArgs);
|
|
2968
3040
|
const api = await connect(subArgs, injectedApi);
|
|
@@ -2973,7 +3045,7 @@ async function handleRunners(subArgs, injectedApi) {
|
|
|
2973
3045
|
console.log(` ${r.enabled ? "\u2713" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (disabled)"}`);
|
|
2974
3046
|
}
|
|
2975
3047
|
console.log(`
|
|
2976
|
-
\u2713 = offered to the planner. ${
|
|
3048
|
+
\u2713 = offered to the planner. ${USAGE7}`);
|
|
2977
3049
|
return;
|
|
2978
3050
|
}
|
|
2979
3051
|
const state = await api.getRunners();
|
|
@@ -2982,7 +3054,7 @@ async function handleRunners(subArgs, injectedApi) {
|
|
|
2982
3054
|
fail(`Unknown runner: ${runner}`, `Enabled or not, this daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
|
|
2983
3055
|
}
|
|
2984
3056
|
const enabled = toggle === void 0 ? !known.enabled : toggle === "on" ? true : toggle === "off" ? false : null;
|
|
2985
|
-
if (enabled === null) fail(
|
|
3057
|
+
if (enabled === null) fail(USAGE7);
|
|
2986
3058
|
await api.setRunnerEnabled(runner, enabled);
|
|
2987
3059
|
console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
|
|
2988
3060
|
}
|
|
@@ -3032,7 +3104,7 @@ async function handleApprove(subArgs, injectedApi) {
|
|
|
3032
3104
|
// src/tui/terminalLauncher.ts
|
|
3033
3105
|
var import_child_process3 = require("child_process");
|
|
3034
3106
|
var import_util = require("util");
|
|
3035
|
-
var
|
|
3107
|
+
var import_core16 = require("@ordewell/core");
|
|
3036
3108
|
var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
|
|
3037
3109
|
var defaultExecFile = async (command, args) => {
|
|
3038
3110
|
const { stdout, stderr } = await execFileAsync(command, args);
|
|
@@ -3052,16 +3124,16 @@ var defaultWhich = (bin) => {
|
|
|
3052
3124
|
async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
|
|
3053
3125
|
const platform = deps.platform ?? process.platform;
|
|
3054
3126
|
const env = deps.env ?? process.env;
|
|
3055
|
-
const hasTmuxImpl = deps.hasTmuxImpl ??
|
|
3127
|
+
const hasTmuxImpl = deps.hasTmuxImpl ?? import_core16.hasTmux;
|
|
3056
3128
|
const execFileImpl = deps.execFileImpl ?? defaultExecFile;
|
|
3057
3129
|
const spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
3058
3130
|
const which = deps.which ?? defaultWhich;
|
|
3059
3131
|
if (!hasTmuxImpl()) {
|
|
3060
3132
|
return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
|
|
3061
3133
|
}
|
|
3062
|
-
const session = (0,
|
|
3063
|
-
const socket = (0,
|
|
3064
|
-
const base = (0,
|
|
3134
|
+
const session = (0, import_core16.tmuxSessionName)(port);
|
|
3135
|
+
const socket = (0, import_core16.tmuxSocketName)(port);
|
|
3136
|
+
const base = (0, import_core16.tmuxWindowName)(taskId, planSessionId);
|
|
3065
3137
|
let windows;
|
|
3066
3138
|
try {
|
|
3067
3139
|
const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
|
|
@@ -3355,7 +3427,7 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
3355
3427
|
}
|
|
3356
3428
|
|
|
3357
3429
|
// src/tui/index.ts
|
|
3358
|
-
var
|
|
3430
|
+
var import_core22 = require("@ordewell/core");
|
|
3359
3431
|
|
|
3360
3432
|
// src/tui/editor.ts
|
|
3361
3433
|
function emptyEditor() {
|
|
@@ -3517,6 +3589,7 @@ var SLASH_COMMANDS = [
|
|
|
3517
3589
|
{ name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
|
|
3518
3590
|
{ name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
|
|
3519
3591
|
{ name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
|
|
3592
|
+
{ name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
|
|
3520
3593
|
{ name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
|
|
3521
3594
|
// Skills
|
|
3522
3595
|
{ name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
|
|
@@ -3525,7 +3598,7 @@ var SLASH_COMMANDS = [
|
|
|
3525
3598
|
{ name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
|
|
3526
3599
|
{ name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
|
|
3527
3600
|
{ name: "compact", usage: "/compact", description: "Condense this conversation into a summary; the last two exchanges and all tasks are kept", category: "session" },
|
|
3528
|
-
{ name: "rewind", usage: "/rewind [<message>]", description: "
|
|
3601
|
+
{ name: "rewind", usage: "/rewind [<message>]", description: "Fork the conversation from just before one of your messages, after a confirmation (the original is kept)", category: "session" },
|
|
3529
3602
|
{ name: "new", usage: "/new", description: "Start a new session", category: "session" },
|
|
3530
3603
|
{ name: "save", usage: "/save", description: "Save the current session", category: "session" },
|
|
3531
3604
|
{ name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
|
|
@@ -3741,6 +3814,7 @@ function chatEditorRoom(cols, active) {
|
|
|
3741
3814
|
}
|
|
3742
3815
|
|
|
3743
3816
|
// src/tui/state.ts
|
|
3817
|
+
var import_core17 = require("@ordewell/core");
|
|
3744
3818
|
var SKILL_IDS = ["tdd", "verify"];
|
|
3745
3819
|
function noSkills() {
|
|
3746
3820
|
return { tdd: false, verify: false };
|
|
@@ -3805,6 +3879,7 @@ function initialState(overrides = {}) {
|
|
|
3805
3879
|
orchestratorModel: "",
|
|
3806
3880
|
plannerProvider: "",
|
|
3807
3881
|
plannerEffort: "",
|
|
3882
|
+
maxParallel: import_core17.DEFAULT_MAX_PARALLEL,
|
|
3808
3883
|
configuredProviders: [],
|
|
3809
3884
|
allowlist: {},
|
|
3810
3885
|
autonomous: true,
|
|
@@ -3824,7 +3899,7 @@ function initialState(overrides = {}) {
|
|
|
3824
3899
|
}
|
|
3825
3900
|
|
|
3826
3901
|
// src/tui/layout.ts
|
|
3827
|
-
var
|
|
3902
|
+
var import_core18 = require("@ordewell/core");
|
|
3828
3903
|
var CHROME_ROWS = 4;
|
|
3829
3904
|
function chatInputWrap(state) {
|
|
3830
3905
|
const active = state.focus === "chat" && !state.overlay;
|
|
@@ -3951,21 +4026,18 @@ function chatBodyLines(messages, cols) {
|
|
|
3951
4026
|
}
|
|
3952
4027
|
function chatLayout(state, rows, cols) {
|
|
3953
4028
|
const welcome = state.tasks.length === 0;
|
|
3954
|
-
if (welcome && state.messages.length === 0) {
|
|
3955
|
-
return { lines: welcomeLines(state, cols), anchor: "top", maxScroll: 0 };
|
|
3956
|
-
}
|
|
3957
4029
|
const body = chatBodyLines(state.messages, cols);
|
|
3958
|
-
const lines = welcome ? [...welcomeLines(state, cols), "", ...body]
|
|
3959
|
-
return { lines, anchor: "bottom", maxScroll: Math.max(0, lines.length - rows) };
|
|
4030
|
+
const lines = !welcome ? body : state.messages.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
|
|
4031
|
+
return { lines, anchor: lines.length > rows ? "bottom" : "top", maxScroll: Math.max(0, lines.length - rows) };
|
|
3960
4032
|
}
|
|
3961
4033
|
function chatScrollMax(state) {
|
|
3962
4034
|
return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
|
|
3963
4035
|
}
|
|
3964
4036
|
function plannerSetup(state) {
|
|
3965
4037
|
const provider = state.plannerProvider;
|
|
3966
|
-
const registration =
|
|
4038
|
+
const registration = import_core18.ALL_PROVIDERS[provider];
|
|
3967
4039
|
if (!registration) return { label: "not set", ready: false };
|
|
3968
|
-
const runner = (0,
|
|
4040
|
+
const runner = (0, import_core18.runnerForProvider)(provider);
|
|
3969
4041
|
const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
|
|
3970
4042
|
return { label: registration.label, ready };
|
|
3971
4043
|
}
|
|
@@ -4073,33 +4145,69 @@ var STATUS_ICON = {
|
|
|
4073
4145
|
pending: "\xB7"
|
|
4074
4146
|
};
|
|
4075
4147
|
var IDLE_ICON = "~";
|
|
4148
|
+
var FIRST_ROW_LINE = 2;
|
|
4076
4149
|
function planLayout(state, rows, cols) {
|
|
4077
4150
|
const done = state.tasks.filter((t) => t.status === "completed").length;
|
|
4078
4151
|
const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
|
|
4079
|
-
|
|
4152
|
+
const rowSpans = [];
|
|
4080
4153
|
let editorLine;
|
|
4081
4154
|
planRows(state).forEach((row, i) => {
|
|
4082
4155
|
const taskStart = lines.length;
|
|
4083
|
-
if (i === state.selectedTask) selectedLine = taskStart;
|
|
4084
4156
|
const renderedTask = taskLines(state, row, i, cols);
|
|
4085
4157
|
if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
|
|
4086
4158
|
editorLine = taskStart + renderedTask.editorLine;
|
|
4087
4159
|
}
|
|
4088
4160
|
lines.push(...renderedTask.lines);
|
|
4161
|
+
rowSpans.push({ start: taskStart, end: lines.length - 1 });
|
|
4089
4162
|
});
|
|
4090
|
-
const
|
|
4091
|
-
const
|
|
4092
|
-
return { lines,
|
|
4163
|
+
const selected = rowSpans[state.selectedTask] ?? { start: FIRST_ROW_LINE, end: FIRST_ROW_LINE };
|
|
4164
|
+
const anchor = editorLine === void 0 ? selected : { start: editorLine, end: editorLine };
|
|
4165
|
+
return { lines, rowSpans, anchor, rows, maxScroll: Math.max(0, lines.length - rows) };
|
|
4166
|
+
}
|
|
4167
|
+
var clampOffset = (layout, offset) => Math.max(0, Math.min(layout.maxScroll, offset));
|
|
4168
|
+
function revealOffset(layout, offset, span) {
|
|
4169
|
+
const at = clampOffset(layout, offset);
|
|
4170
|
+
const bodyHeight = layout.rows - 1;
|
|
4171
|
+
const first = span.start <= FIRST_ROW_LINE ? 0 : span.start - 1;
|
|
4172
|
+
if (span.start < 1 + at || span.end - span.start + 1 > bodyHeight) return clampOffset(layout, first);
|
|
4173
|
+
if (span.end > at + layout.rows - 1) return clampOffset(layout, span.end - layout.rows + 1);
|
|
4174
|
+
return at;
|
|
4093
4175
|
}
|
|
4094
4176
|
function planOffset(layout, planScroll) {
|
|
4095
|
-
if (planScroll === null) return layout.
|
|
4096
|
-
return
|
|
4177
|
+
if (planScroll === null) return revealOffset(layout, 0, layout.anchor);
|
|
4178
|
+
return clampOffset(layout, planScroll);
|
|
4179
|
+
}
|
|
4180
|
+
function topRowInView(layout, offset) {
|
|
4181
|
+
const top = 1 + offset;
|
|
4182
|
+
const whole = layout.rowSpans.findIndex((span) => span.start >= top && span.end <= offset + layout.rows - 1);
|
|
4183
|
+
if (whole >= 0) return whole;
|
|
4184
|
+
const covering = layout.rowSpans.findIndex((span) => span.end >= top);
|
|
4185
|
+
return covering >= 0 ? covering : layout.rowSpans.length - 1;
|
|
4186
|
+
}
|
|
4187
|
+
function bottomRowInView(layout, offset) {
|
|
4188
|
+
const bottom = offset + layout.rows - 1;
|
|
4189
|
+
const spans = layout.rowSpans;
|
|
4190
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
4191
|
+
if (spans[i].start >= 1 + offset && spans[i].end <= bottom) return i;
|
|
4192
|
+
}
|
|
4193
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
4194
|
+
if (spans[i].start <= bottom) return i;
|
|
4195
|
+
}
|
|
4196
|
+
return 0;
|
|
4197
|
+
}
|
|
4198
|
+
function rowNearestView(layout, offset, row) {
|
|
4199
|
+
const span = layout.rowSpans[row];
|
|
4200
|
+
if (!span) return row;
|
|
4201
|
+
if (span.end < 1 + offset) return topRowInView(layout, offset);
|
|
4202
|
+
if (span.start > offset + layout.rows - 1) return bottomRowInView(layout, offset);
|
|
4203
|
+
return row;
|
|
4097
4204
|
}
|
|
4098
4205
|
function planScrollExtent(state) {
|
|
4099
4206
|
const cols = planPaneWidth(state);
|
|
4100
|
-
if (cols === 0)
|
|
4101
|
-
|
|
4102
|
-
|
|
4207
|
+
if (cols === 0) {
|
|
4208
|
+
return { lines: [], rowSpans: [], anchor: { start: FIRST_ROW_LINE, end: FIRST_ROW_LINE }, rows: 0, maxScroll: 0 };
|
|
4209
|
+
}
|
|
4210
|
+
return planLayout(state, bodyRows(state), cols);
|
|
4103
4211
|
}
|
|
4104
4212
|
function taskLines(state, row, index, cols) {
|
|
4105
4213
|
const { task, parent } = row;
|
|
@@ -4112,7 +4220,7 @@ function taskLines(state, row, index, cols) {
|
|
|
4112
4220
|
const icon = paint(rawIcon);
|
|
4113
4221
|
const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
|
|
4114
4222
|
const caret = selected ? style.cyan("\u276F") : " ";
|
|
4115
|
-
const orderLabel = parent ? (0,
|
|
4223
|
+
const orderLabel = parent ? (0, import_core18.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
|
|
4116
4224
|
const indent = parent ? " " : "";
|
|
4117
4225
|
const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
|
|
4118
4226
|
const titleRoom = Math.max(1, cols - width(head));
|
|
@@ -4133,12 +4241,22 @@ function taskLines(state, row, index, cols) {
|
|
|
4133
4241
|
const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
|
|
4134
4242
|
const runner = task.assignedRunner ?? "";
|
|
4135
4243
|
const bodyPad = parent ? " " : " ";
|
|
4136
|
-
const
|
|
4244
|
+
const activity = idle ? "quiet \u2014 t opens its terminal" : running ? "working" : "";
|
|
4245
|
+
const meta = [activity, runner, model].filter(Boolean).join(" \xB7 ");
|
|
4137
4246
|
if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
|
|
4138
4247
|
if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
|
|
4139
4248
|
if (task.isolation?.state === "conflict") {
|
|
4140
4249
|
const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
|
|
4141
|
-
|
|
4250
|
+
const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core18.capConflictFiles)(task.isolation.conflictFiles)})` : "";
|
|
4251
|
+
lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
|
|
4252
|
+
}
|
|
4253
|
+
if (task.isolation?.state === "repairing") {
|
|
4254
|
+
const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core18.capConflictFiles)(task.isolation.conflictFiles)}` : "";
|
|
4255
|
+
const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
|
|
4256
|
+
lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
|
|
4257
|
+
}
|
|
4258
|
+
if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
|
|
4259
|
+
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core18.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
|
|
4142
4260
|
}
|
|
4143
4261
|
let editorLine;
|
|
4144
4262
|
if (expanded) {
|
|
@@ -4223,6 +4341,7 @@ function helpScrollMax(state) {
|
|
|
4223
4341
|
}
|
|
4224
4342
|
|
|
4225
4343
|
// src/tui/render.ts
|
|
4344
|
+
var import_core19 = require("@ordewell/core");
|
|
4226
4345
|
function render(state) {
|
|
4227
4346
|
const { rows, cols } = state;
|
|
4228
4347
|
const rowsForBody = bodyRows(state);
|
|
@@ -4321,8 +4440,7 @@ function renderSkills(state, cols) {
|
|
|
4321
4440
|
return truncate([...badges, auto].join(" "), cols);
|
|
4322
4441
|
}
|
|
4323
4442
|
function scrolledBackMark(state) {
|
|
4324
|
-
|
|
4325
|
-
return state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
|
|
4443
|
+
return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
|
|
4326
4444
|
}
|
|
4327
4445
|
function renderStatus(state, cols) {
|
|
4328
4446
|
const mark = scrolledBackMark(state);
|
|
@@ -4462,14 +4580,7 @@ function renderOverlay(state, rows, cols) {
|
|
|
4462
4580
|
cols
|
|
4463
4581
|
);
|
|
4464
4582
|
}
|
|
4465
|
-
if (overlay.kind === "confirm")
|
|
4466
|
-
return frame(
|
|
4467
|
-
overlay.title,
|
|
4468
|
-
[overlay.message, "", style.grey("enter confirms \xB7 esc cancels")],
|
|
4469
|
-
rows,
|
|
4470
|
-
cols
|
|
4471
|
-
);
|
|
4472
|
-
}
|
|
4583
|
+
if (overlay.kind === "confirm") return renderConfirm(overlay, rows, cols);
|
|
4473
4584
|
if (overlay.kind === "prompt") {
|
|
4474
4585
|
const secret = overlay.action.kind === "api-key";
|
|
4475
4586
|
const shown = secret ? "\u2022".repeat(overlay.value.length) : overlay.value;
|
|
@@ -4493,7 +4604,10 @@ function renderHandoff(state, overlay, rows, cols) {
|
|
|
4493
4604
|
const more = scroll + room < lines.length ? "\u2191\u2193 pgup/pgdn scroll \xB7 " : "";
|
|
4494
4605
|
return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
|
|
4495
4606
|
}
|
|
4496
|
-
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) =>
|
|
4607
|
+
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
|
|
4608
|
+
const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core19.capConflictFiles)(task.repairedFiles)})`) : "";
|
|
4609
|
+
return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
|
|
4610
|
+
});
|
|
4497
4611
|
const actions = handoffActions(handoff).map((action, index) => {
|
|
4498
4612
|
const active = index === overlay.index;
|
|
4499
4613
|
const caret = active ? style.cyan("\u276F") : " ";
|
|
@@ -4553,6 +4667,24 @@ function renderHelp(scroll, rows, cols) {
|
|
|
4553
4667
|
const footer = style.grey(offset < maxScroll ? "\u2191\u2193 scroll \xB7 any other key closes" : "any key closes");
|
|
4554
4668
|
return frame("Commands", [...lines.slice(offset, offset + room), footer], rows, cols);
|
|
4555
4669
|
}
|
|
4670
|
+
var QUOTE_MAX_LINES = 8;
|
|
4671
|
+
function renderConfirm(overlay, rows, cols) {
|
|
4672
|
+
const { choice, quote, note } = overlay;
|
|
4673
|
+
const inner = Math.max(1, cols - 3);
|
|
4674
|
+
const options = (choice?.options ?? []).map((o, i) => `${i === choice?.index ? style.cyan("\u276F") : " "} ${i + 1}. ${o.label}`);
|
|
4675
|
+
const hint = choice ? style.grey(`\u2191\u2193 move \xB7 enter or 1\u2013${choice.options.length} chooses \xB7 esc cancels`) : style.grey("enter confirms \xB7 esc cancels");
|
|
4676
|
+
const noteLines = note ? ["", ...note.split("\n")] : [];
|
|
4677
|
+
const rest = [...noteLines, ...options.length > 0 ? ["", ...options] : [], "", hint];
|
|
4678
|
+
const quoted = [];
|
|
4679
|
+
if (quote !== void 0) {
|
|
4680
|
+
const fixed = 1 + wrap(overlay.message, inner).length + 1 + 1 + rest.flatMap((l) => wrap(l, inner)).length;
|
|
4681
|
+
const room = Math.max(2, Math.min(QUOTE_MAX_LINES, rows - fixed));
|
|
4682
|
+
const wrapped = wrap(quote, inner - 2);
|
|
4683
|
+
const shown = wrapped.length > room ? [...wrapped.slice(0, room - 1), "\u2026"] : wrapped;
|
|
4684
|
+
quoted.push("", ...shown.map((l) => `${style.grey("\u2502")} ${l}`));
|
|
4685
|
+
}
|
|
4686
|
+
return frame(overlay.title, [overlay.message, ...quoted, ...rest], rows, cols);
|
|
4687
|
+
}
|
|
4556
4688
|
function frame(title, body, rows, cols) {
|
|
4557
4689
|
const inner = Math.max(1, cols - 2);
|
|
4558
4690
|
const lines = [
|
|
@@ -4563,9 +4695,9 @@ function frame(title, body, rows, cols) {
|
|
|
4563
4695
|
}
|
|
4564
4696
|
|
|
4565
4697
|
// src/tui/reducer.ts
|
|
4566
|
-
var
|
|
4698
|
+
var import_core20 = require("@ordewell/core");
|
|
4567
4699
|
var import_plan_utils3 = require("@ordewell/core/plan-utils");
|
|
4568
|
-
var KNOWN_PROVIDERS2 = Object.keys(
|
|
4700
|
+
var KNOWN_PROVIDERS2 = Object.keys(import_core20.ALL_PROVIDERS);
|
|
4569
4701
|
var DEFAULT_EFFORT = "__runner_default__";
|
|
4570
4702
|
function step(state, effects = []) {
|
|
4571
4703
|
return { state, effects };
|
|
@@ -4584,6 +4716,17 @@ function alreadySpoken(messages, content) {
|
|
|
4584
4716
|
const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
|
|
4585
4717
|
return spoken || redrawn;
|
|
4586
4718
|
}
|
|
4719
|
+
function runLabel(tasks) {
|
|
4720
|
+
const running = tasks.filter(isTaskRunning);
|
|
4721
|
+
if (running.length > 0) {
|
|
4722
|
+
const [first] = running;
|
|
4723
|
+
const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
|
|
4724
|
+
return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
|
|
4725
|
+
}
|
|
4726
|
+
const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
|
|
4727
|
+
if (waiting === 0) return "";
|
|
4728
|
+
return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
|
|
4729
|
+
}
|
|
4587
4730
|
function researchLabel(messages, summary) {
|
|
4588
4731
|
const others = messages.filter(isPendingResearch).length - 1;
|
|
4589
4732
|
return others > 0 ? `${summary} (+${others} more)` : summary;
|
|
@@ -4653,7 +4796,8 @@ function reduce(state, action) {
|
|
|
4653
4796
|
expandedTaskId,
|
|
4654
4797
|
taskEditor: expandedTaskId !== null ? state.taskEditor : null
|
|
4655
4798
|
};
|
|
4656
|
-
|
|
4799
|
+
const selectedTask = clampSelection(state.selectedTask, planRows(next).length);
|
|
4800
|
+
return step(settlePlan({ ...next, selectedTask, focus: tasks.length === 0 ? "chat" : state.focus }));
|
|
4657
4801
|
}
|
|
4658
4802
|
case "plannerMessage": {
|
|
4659
4803
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -4686,6 +4830,16 @@ function reduce(state, action) {
|
|
|
4686
4830
|
const status = state.status === "planning" ? "researching" : state.status;
|
|
4687
4831
|
return step({ ...spoken, status, busyLabel: researchLabel(spoken.messages, summary) });
|
|
4688
4832
|
}
|
|
4833
|
+
// A settled transcript line, not a research step: nothing ever settles a
|
|
4834
|
+
// task's start, so as a step it stayed "⋯" forever and was counted into
|
|
4835
|
+
// the planner's next "(+N more)".
|
|
4836
|
+
case "taskStarted": {
|
|
4837
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4838
|
+
const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
|
|
4839
|
+
const who = action.runner ? ` \xB7 ${action.runner}` : "";
|
|
4840
|
+
const spoken = say(state, "system", sanitize(`Started "${action.title}"${who}`));
|
|
4841
|
+
return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
|
|
4842
|
+
}
|
|
4689
4843
|
case "researchStepDone": {
|
|
4690
4844
|
if (stale(state, action.sessionId)) return step(state);
|
|
4691
4845
|
const messages = settleResearchStep(state.messages, action);
|
|
@@ -4726,7 +4880,7 @@ function reduce(state, action) {
|
|
|
4726
4880
|
return t;
|
|
4727
4881
|
});
|
|
4728
4882
|
if (!changed) return step(state);
|
|
4729
|
-
return step({ ...state, status: "executing", tasks });
|
|
4883
|
+
return step({ ...state, status: "executing", tasks, busyLabel: runLabel(tasks) });
|
|
4730
4884
|
}
|
|
4731
4885
|
case "queueReady": {
|
|
4732
4886
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -4746,7 +4900,7 @@ function reduce(state, action) {
|
|
|
4746
4900
|
if (stale(state, action.sessionId)) return step(state);
|
|
4747
4901
|
return step(showDiff(state, action.diff));
|
|
4748
4902
|
}
|
|
4749
|
-
case "
|
|
4903
|
+
case "runCleared": {
|
|
4750
4904
|
if (stale(state, action.sessionId)) return step(state);
|
|
4751
4905
|
const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
|
|
4752
4906
|
return step({ ...clearIsolation(state), overlay });
|
|
@@ -4787,6 +4941,7 @@ function reduce(state, action) {
|
|
|
4787
4941
|
return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
|
|
4788
4942
|
case "rewindTargetsLoaded":
|
|
4789
4943
|
if (stale(state, action.sessionId)) return step(state);
|
|
4944
|
+
if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
|
|
4790
4945
|
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
4791
4946
|
// A fork holds no run, whatever the session it came from was doing — the
|
|
4792
4947
|
// original keeps its run, and its events are stale from here on.
|
|
@@ -4802,6 +4957,15 @@ function reduce(state, action) {
|
|
|
4802
4957
|
pendingApprovals: [],
|
|
4803
4958
|
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
4804
4959
|
});
|
|
4960
|
+
// The rewound message comes back as a draft to edit and resend, so it lands
|
|
4961
|
+
// as if just typed: history is not being browsed, and nothing is parked.
|
|
4962
|
+
case "inputPrefilled":
|
|
4963
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4964
|
+
return step({
|
|
4965
|
+
...state,
|
|
4966
|
+
focus: "chat",
|
|
4967
|
+
editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
4968
|
+
});
|
|
4805
4969
|
case "runnersLoaded":
|
|
4806
4970
|
return step(refillPicker({
|
|
4807
4971
|
...state,
|
|
@@ -4827,11 +4991,7 @@ function reduce(state, action) {
|
|
|
4827
4991
|
return step(say(state, "system", action.message));
|
|
4828
4992
|
case "resize": {
|
|
4829
4993
|
const resized = { ...state, rows: action.rows, cols: action.cols, selection: null };
|
|
4830
|
-
return step({
|
|
4831
|
-
...resized,
|
|
4832
|
-
scroll: clamp(resized.scroll, chatScrollMax(resized)),
|
|
4833
|
-
planScroll: resized.planScroll === null ? null : clamp(resized.planScroll, planScrollExtent(resized).maxScroll)
|
|
4834
|
-
});
|
|
4994
|
+
return step(settlePlan({ ...resized, scroll: clamp(resized.scroll, chatScrollMax(resized)) }));
|
|
4835
4995
|
}
|
|
4836
4996
|
// Whether there is anything to animate is the app loop's call (it owns the
|
|
4837
4997
|
// timer); a tick that arrives simply advances the frame.
|
|
@@ -4890,6 +5050,7 @@ function applySettings(state, settings) {
|
|
|
4890
5050
|
orchestratorModel: typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : state.orchestratorModel,
|
|
4891
5051
|
plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
|
|
4892
5052
|
plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
|
|
5053
|
+
maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
|
|
4893
5054
|
allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
|
|
4894
5055
|
};
|
|
4895
5056
|
}
|
|
@@ -4906,11 +5067,11 @@ function modelDetail(m) {
|
|
|
4906
5067
|
function providerErrorHint(state) {
|
|
4907
5068
|
const failed = Object.keys(state.providerErrors ?? {});
|
|
4908
5069
|
if (failed.length === 0) return void 0;
|
|
4909
|
-
const names = failed.map((p) =>
|
|
5070
|
+
const names = failed.map((p) => import_core20.ALL_PROVIDERS[p]?.label ?? p);
|
|
4910
5071
|
return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
|
|
4911
5072
|
}
|
|
4912
5073
|
function plannerModelItems(state) {
|
|
4913
|
-
const runner = (0,
|
|
5074
|
+
const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
|
|
4914
5075
|
if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
|
|
4915
5076
|
const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
|
|
4916
5077
|
id: m.id,
|
|
@@ -4924,7 +5085,7 @@ function plannerModelItems(state) {
|
|
|
4924
5085
|
return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
|
|
4925
5086
|
}
|
|
4926
5087
|
function plannerEffortItems(state) {
|
|
4927
|
-
const runner = (0,
|
|
5088
|
+
const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
|
|
4928
5089
|
if (!runner) {
|
|
4929
5090
|
return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
|
|
4930
5091
|
}
|
|
@@ -5049,7 +5210,7 @@ function pointerPane(state, key) {
|
|
|
5049
5210
|
}
|
|
5050
5211
|
function scrollPointed(state, key) {
|
|
5051
5212
|
const delta = key.name === "scrollup" ? WHEEL_NOTCH : -WHEEL_NOTCH;
|
|
5052
|
-
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta) : scrollChat(state, delta);
|
|
5213
|
+
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta, false) : scrollChat(state, delta);
|
|
5053
5214
|
}
|
|
5054
5215
|
var isMouseSelect = (key) => key.name === "mousedown" || key.name === "mousedrag" || key.name === "mouseup";
|
|
5055
5216
|
function selectionCell(state, pane, key) {
|
|
@@ -5082,10 +5243,23 @@ function clamp(value, max) {
|
|
|
5082
5243
|
function scrollChat(state, delta) {
|
|
5083
5244
|
return step({ ...state, scroll: clamp(state.scroll + delta, chatScrollMax(state)) });
|
|
5084
5245
|
}
|
|
5085
|
-
function
|
|
5086
|
-
|
|
5087
|
-
const
|
|
5088
|
-
return
|
|
5246
|
+
function settlePlan(state) {
|
|
5247
|
+
if (planPaneWidth(state) === 0) return state;
|
|
5248
|
+
const layout = planScrollExtent(state);
|
|
5249
|
+
return { ...state, planScroll: revealOffset(layout, planOffset(layout, state.planScroll), layout.anchor) };
|
|
5250
|
+
}
|
|
5251
|
+
function scrollPlan(state, delta, paged) {
|
|
5252
|
+
const layout = planScrollExtent(state);
|
|
5253
|
+
const from = planOffset(layout, state.planScroll);
|
|
5254
|
+
const to = clamp(from - delta, layout.maxScroll);
|
|
5255
|
+
const scrolled = { ...state, planScroll: to };
|
|
5256
|
+
if (state.taskEditor || layout.rowSpans.length === 0) return step(scrolled);
|
|
5257
|
+
const back = delta > 0;
|
|
5258
|
+
if (!paged) {
|
|
5259
|
+
return step({ ...scrolled, selectedTask: rowNearestView(layout, to, state.selectedTask) });
|
|
5260
|
+
}
|
|
5261
|
+
if (to === from) return step({ ...scrolled, selectedTask: back ? 0 : layout.rowSpans.length - 1 });
|
|
5262
|
+
return step({ ...scrolled, selectedTask: back ? bottomRowInView(layout, to) : topRowInView(layout, to) });
|
|
5089
5263
|
}
|
|
5090
5264
|
var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
|
|
5091
5265
|
function stopPlanning(state, sessionId) {
|
|
@@ -5115,7 +5289,10 @@ function handleKey2(state, key) {
|
|
|
5115
5289
|
return step({ ...state, editor: { ...state.editor, text, cursor: token.start + completed.length } });
|
|
5116
5290
|
}
|
|
5117
5291
|
}
|
|
5118
|
-
if (key.name === "tab")
|
|
5292
|
+
if (key.name === "tab") {
|
|
5293
|
+
if (state.focus === "chat" && state.tasks.length === 0) return step(state);
|
|
5294
|
+
return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
|
|
5295
|
+
}
|
|
5119
5296
|
if (state.focus === "plan") return handlePlanKey(state, key);
|
|
5120
5297
|
if (key.name === "enter") return submit(state);
|
|
5121
5298
|
if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
@@ -5147,35 +5324,32 @@ function handlePlanKey(state, key) {
|
|
|
5147
5324
|
const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
|
|
5148
5325
|
if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
|
|
5149
5326
|
if (key.name === "escape") {
|
|
5150
|
-
if (state.expandedTaskId) return step({ ...state, expandedTaskId: null
|
|
5327
|
+
if (state.expandedTaskId) return step(settlePlan({ ...state, expandedTaskId: null }));
|
|
5151
5328
|
return step({ ...state, focus: "chat" });
|
|
5152
5329
|
}
|
|
5153
5330
|
if (key.name === "up") {
|
|
5154
|
-
return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1)
|
|
5331
|
+
return step(settlePlan({ ...state, selectedTask: Math.max(0, state.selectedTask - 1) }));
|
|
5155
5332
|
}
|
|
5156
5333
|
if (key.name === "down") {
|
|
5157
|
-
return step({
|
|
5158
|
-
...state,
|
|
5159
|
-
selectedTask: Math.min(planRows(state).length - 1, state.selectedTask + 1),
|
|
5160
|
-
planScroll: null
|
|
5161
|
-
});
|
|
5334
|
+
return step(settlePlan({ ...state, selectedTask: clampSelection(state.selectedTask + 1, planRows(state).length) }));
|
|
5162
5335
|
}
|
|
5163
5336
|
const scroll = scrollDelta(key, state);
|
|
5164
|
-
if (scroll !== null) return scrollPlan(state, scroll);
|
|
5337
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5338
|
+
if (key.name === "char" && key.char === "a") return addTask(state, "");
|
|
5165
5339
|
const row = selectedPlanRow(state);
|
|
5166
5340
|
if (!row) return step(state);
|
|
5167
5341
|
const task = row.task;
|
|
5168
5342
|
if (key.name === "enter" || key.name === "right") {
|
|
5169
5343
|
const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
|
|
5170
5344
|
if (hasSubtasks && state.expandedTaskId !== task.id) {
|
|
5171
|
-
return step({ ...state, expandedTaskId: task.id, taskEditor: null
|
|
5345
|
+
return step(settlePlan({ ...state, expandedTaskId: task.id, taskEditor: null }));
|
|
5172
5346
|
}
|
|
5173
5347
|
const text = task.prompt ?? task.description ?? task.title;
|
|
5174
|
-
return step({
|
|
5348
|
+
return step(settlePlan({
|
|
5175
5349
|
...state,
|
|
5176
5350
|
expandedTaskId: task.id,
|
|
5177
5351
|
taskEditor: { ...emptyEditor(), text, cursor: text.length }
|
|
5178
|
-
});
|
|
5352
|
+
}));
|
|
5179
5353
|
}
|
|
5180
5354
|
if (!state.sessionId || key.name !== "char") return step(state);
|
|
5181
5355
|
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
@@ -5184,7 +5358,6 @@ function handlePlanKey(state, key) {
|
|
|
5184
5358
|
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
5185
5359
|
}
|
|
5186
5360
|
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
5187
|
-
if (key.char === "a") return addTask(state, "");
|
|
5188
5361
|
if (key.char === "t") {
|
|
5189
5362
|
return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
|
|
5190
5363
|
}
|
|
@@ -5200,13 +5373,13 @@ function handlePlanKey(state, key) {
|
|
|
5200
5373
|
}
|
|
5201
5374
|
function handleTaskEditKey(state, task, editor, key) {
|
|
5202
5375
|
if (key.name === "enter") return commitTaskEdit(state, task, editor);
|
|
5203
|
-
if (key.name === "escape") return step({ ...state, expandedTaskId: null, taskEditor: null });
|
|
5376
|
+
if (key.name === "escape") return step(settlePlan({ ...state, expandedTaskId: null, taskEditor: null }));
|
|
5204
5377
|
const scroll = scrollDelta(key, state);
|
|
5205
|
-
if (scroll !== null) return scrollPlan(state, scroll);
|
|
5206
|
-
return step({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) });
|
|
5378
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5379
|
+
return step(settlePlan({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) }));
|
|
5207
5380
|
}
|
|
5208
5381
|
function commitTaskEdit(state, task, editor) {
|
|
5209
|
-
const collapsed = { ...state, expandedTaskId: null, taskEditor: null };
|
|
5382
|
+
const collapsed = settlePlan({ ...state, expandedTaskId: null, taskEditor: null });
|
|
5210
5383
|
const prompt = editor.text.trim();
|
|
5211
5384
|
const original = task.prompt ?? task.description ?? task.title;
|
|
5212
5385
|
if (!prompt || prompt === original || !state.sessionId) return step(collapsed);
|
|
@@ -5261,7 +5434,18 @@ function handleOverlayKey(state, overlay, key) {
|
|
|
5261
5434
|
}
|
|
5262
5435
|
function handleConfirmKey(state, overlay, key) {
|
|
5263
5436
|
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
5264
|
-
|
|
5437
|
+
const { choice } = overlay;
|
|
5438
|
+
if (!choice) return key.name === "enter" ? runConfirm(state, overlay) : step(state);
|
|
5439
|
+
if (key.name === "up" || key.name === "down") {
|
|
5440
|
+
const index = clamp(choice.index + (key.name === "down" ? 1 : -1), choice.options.length - 1);
|
|
5441
|
+
return step({ ...state, overlay: { ...overlay, choice: { ...choice, index } } });
|
|
5442
|
+
}
|
|
5443
|
+
const chosen = key.name === "enter" ? choice.index : key.name === "char" ? Number(key.char) - 1 : -1;
|
|
5444
|
+
const option = choice.options[chosen];
|
|
5445
|
+
if (!option) return step(state);
|
|
5446
|
+
return option.confirms ? runConfirm(state, overlay) : step({ ...state, overlay: null });
|
|
5447
|
+
}
|
|
5448
|
+
function runConfirm(state, overlay) {
|
|
5265
5449
|
const closed = { ...state, overlay: null };
|
|
5266
5450
|
if (overlay.action.kind === "new-session") return newSession(closed);
|
|
5267
5451
|
if (overlay.action.kind === "remove-task") {
|
|
@@ -5274,7 +5458,8 @@ function handleConfirmKey(state, overlay, key) {
|
|
|
5274
5458
|
if (overlay.action.kind === "init-workspace") {
|
|
5275
5459
|
return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
|
|
5276
5460
|
}
|
|
5277
|
-
|
|
5461
|
+
const { index } = overlay.action;
|
|
5462
|
+
return withIdlePlanner(closed, (sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index }]));
|
|
5278
5463
|
}
|
|
5279
5464
|
function handleHelpKey(state, scroll, key) {
|
|
5280
5465
|
const page = pageNotch(bodyRows(state));
|
|
@@ -5298,7 +5483,7 @@ function handlePromptKey(state, overlay, key) {
|
|
|
5298
5483
|
if (overlay.action.kind === "api-key") {
|
|
5299
5484
|
return step(closed, [{ type: "setApiKey", provider: overlay.action.provider, key: value2 }]);
|
|
5300
5485
|
}
|
|
5301
|
-
if (!state.sessionId) return fail3(closed, "No active plan to add a task to
|
|
5486
|
+
if (!state.sessionId) return fail3(closed, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
5302
5487
|
return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
|
|
5303
5488
|
}
|
|
5304
5489
|
const value = editText(overlay.value, key);
|
|
@@ -5371,10 +5556,7 @@ function choose(state, picker2, item) {
|
|
|
5371
5556
|
case "load-session":
|
|
5372
5557
|
return step(closed, [{ type: "loadSession", sessionId: item.id }]);
|
|
5373
5558
|
case "rewind":
|
|
5374
|
-
return
|
|
5375
|
-
closed,
|
|
5376
|
-
(sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index: Number(item.id) }])
|
|
5377
|
-
);
|
|
5559
|
+
return pickRewindTarget(state, Number(item.id));
|
|
5378
5560
|
case "delete-session":
|
|
5379
5561
|
return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
|
|
5380
5562
|
case "set-key":
|
|
@@ -5455,7 +5637,7 @@ function runCommand(state, { name, args }) {
|
|
|
5455
5637
|
case "quit":
|
|
5456
5638
|
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
5457
5639
|
case "refresh":
|
|
5458
|
-
return step(state, [{ type: "refresh" }]);
|
|
5640
|
+
return step(state, [{ type: "refresh", announce: true }]);
|
|
5459
5641
|
case "run":
|
|
5460
5642
|
return withSession(state, (sessionId) => step(state, [{ type: "execute", sessionId }]));
|
|
5461
5643
|
case "approve":
|
|
@@ -5484,6 +5666,8 @@ function runCommand(state, { name, args }) {
|
|
|
5484
5666
|
return runners(state, args);
|
|
5485
5667
|
case "auto":
|
|
5486
5668
|
return setAutonomous(state, args[0]);
|
|
5669
|
+
case "parallel":
|
|
5670
|
+
return setMaxParallel(state, args[0]);
|
|
5487
5671
|
case "mouse":
|
|
5488
5672
|
return setMouseCapture(state, args[0]);
|
|
5489
5673
|
case "sessions":
|
|
@@ -5571,12 +5755,34 @@ function rewind(state, arg) {
|
|
|
5571
5755
|
return withIdlePlanner(state, (sessionId) => {
|
|
5572
5756
|
if (arg === void 0) {
|
|
5573
5757
|
return step(
|
|
5574
|
-
{ ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "
|
|
5758
|
+
{ ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "Rewinding forks the conversation from just before the chosen message; the original is kept and the tasks ride along." }) } },
|
|
5575
5759
|
[{ type: "loadRewindTargets", sessionId }]
|
|
5576
5760
|
);
|
|
5577
5761
|
}
|
|
5578
5762
|
if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
|
|
5579
|
-
return step(state, [{ type: "
|
|
5763
|
+
return step(state, [{ type: "loadRewindTargets", sessionId, pick: Number(arg) }]);
|
|
5764
|
+
});
|
|
5765
|
+
}
|
|
5766
|
+
var REWIND_OPTIONS = [
|
|
5767
|
+
{ label: "Restore Conversation", confirms: true },
|
|
5768
|
+
{ label: "Never mind", confirms: false }
|
|
5769
|
+
];
|
|
5770
|
+
function pickRewindTarget(state, index) {
|
|
5771
|
+
return withIdlePlanner({ ...state, overlay: null }, () => {
|
|
5772
|
+
const target = state.rewindTargets?.find((t) => t.index === index);
|
|
5773
|
+
if (!target) return fail3({ ...state, overlay: null }, `No message ${index} to rewind to \u2014 /rewind alone lists them.`);
|
|
5774
|
+
return step({
|
|
5775
|
+
...state,
|
|
5776
|
+
overlay: {
|
|
5777
|
+
kind: "confirm",
|
|
5778
|
+
title: "Rewind",
|
|
5779
|
+
message: "Confirm you want to restore to the point before you sent this message:",
|
|
5780
|
+
quote: target.content,
|
|
5781
|
+
note: "The conversation will be forked.\nThe code will be unchanged.",
|
|
5782
|
+
action: { kind: "rewind", index },
|
|
5783
|
+
choice: { options: REWIND_OPTIONS, index: 0 }
|
|
5784
|
+
}
|
|
5785
|
+
});
|
|
5580
5786
|
});
|
|
5581
5787
|
}
|
|
5582
5788
|
function resolveToggle(arg, current) {
|
|
@@ -5618,6 +5824,7 @@ function newSession(state) {
|
|
|
5618
5824
|
expandedTaskId: null,
|
|
5619
5825
|
taskEditor: null,
|
|
5620
5826
|
scroll: 0,
|
|
5827
|
+
planScroll: null,
|
|
5621
5828
|
status: "idle",
|
|
5622
5829
|
busyLabel: "",
|
|
5623
5830
|
thinkingLine: "",
|
|
@@ -5629,7 +5836,7 @@ function newSession(state) {
|
|
|
5629
5836
|
}
|
|
5630
5837
|
function setModel(state, args) {
|
|
5631
5838
|
if (args[0] === "set" && args[1]) {
|
|
5632
|
-
const runner = (0,
|
|
5839
|
+
const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
|
|
5633
5840
|
const known = runner ? modelsForRunner(state.models, runner) : [];
|
|
5634
5841
|
if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
|
|
5635
5842
|
return fail3(state, `${args[1]} was not discovered for ${runner}.`);
|
|
@@ -5651,21 +5858,21 @@ function setModel(state, args) {
|
|
|
5651
5858
|
}
|
|
5652
5859
|
function plannerItems(state) {
|
|
5653
5860
|
const installed = new Set(state.runners.map((r) => r.id));
|
|
5654
|
-
const agents =
|
|
5655
|
-
const runner = (0,
|
|
5861
|
+
const agents = import_core20.CLI_PROVIDERS.map((id) => {
|
|
5862
|
+
const runner = (0, import_core20.runnerForProvider)(id);
|
|
5656
5863
|
const ready = installed.has(runner);
|
|
5657
5864
|
return {
|
|
5658
5865
|
id,
|
|
5659
|
-
label:
|
|
5866
|
+
label: import_core20.ALL_PROVIDERS[id].label,
|
|
5660
5867
|
detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
|
|
5661
5868
|
selected: state.plannerProvider === id,
|
|
5662
5869
|
disabled: !ready
|
|
5663
5870
|
};
|
|
5664
5871
|
});
|
|
5665
|
-
const vendors =
|
|
5872
|
+
const vendors = import_core20.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
|
|
5666
5873
|
id,
|
|
5667
|
-
label:
|
|
5668
|
-
detail:
|
|
5874
|
+
label: import_core20.ALL_PROVIDERS[id].label,
|
|
5875
|
+
detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5669
5876
|
selected: state.plannerProvider === id
|
|
5670
5877
|
}));
|
|
5671
5878
|
return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
|
|
@@ -5723,16 +5930,16 @@ function setKey(state, args) {
|
|
|
5723
5930
|
}
|
|
5724
5931
|
return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
|
|
5725
5932
|
}
|
|
5726
|
-
const items =
|
|
5933
|
+
const items = import_core20.PROVIDER_PRIORITY.map((id) => ({
|
|
5727
5934
|
id,
|
|
5728
|
-
label:
|
|
5729
|
-
detail:
|
|
5935
|
+
label: import_core20.ALL_PROVIDERS[id].label,
|
|
5936
|
+
detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5730
5937
|
selected: state.configuredProviders.includes(id)
|
|
5731
5938
|
}));
|
|
5732
5939
|
return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
|
|
5733
5940
|
}
|
|
5734
5941
|
function keyPrompt(provider) {
|
|
5735
|
-
const meta =
|
|
5942
|
+
const meta = import_core20.ALL_PROVIDERS[provider];
|
|
5736
5943
|
return {
|
|
5737
5944
|
kind: "prompt",
|
|
5738
5945
|
title: `${meta.label} API key`,
|
|
@@ -5807,6 +6014,12 @@ function runners(state, args) {
|
|
|
5807
6014
|
})
|
|
5808
6015
|
} });
|
|
5809
6016
|
}
|
|
6017
|
+
function setMaxParallel(state, arg) {
|
|
6018
|
+
if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once \u2014 /parallel <n> changes it.`));
|
|
6019
|
+
const limit = (0, import_core20.parseMaxParallel)(arg);
|
|
6020
|
+
if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
|
|
6021
|
+
return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
|
|
6022
|
+
}
|
|
5810
6023
|
function setAutonomous(state, arg) {
|
|
5811
6024
|
const enabled = resolveToggle(arg, state.autonomous);
|
|
5812
6025
|
if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
|
|
@@ -6009,16 +6222,20 @@ function taskEffortCommand(state, args) {
|
|
|
6009
6222
|
return assignTaskEffort(state, sessionId, task, value);
|
|
6010
6223
|
});
|
|
6011
6224
|
}
|
|
6225
|
+
function clampSelection(index, rows) {
|
|
6226
|
+
return Math.max(0, Math.min(index, rows - 1));
|
|
6227
|
+
}
|
|
6012
6228
|
function addTask(state, title) {
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
}
|
|
6229
|
+
if (!state.sessionId) {
|
|
6230
|
+
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
6231
|
+
}
|
|
6232
|
+
if (!title) {
|
|
6233
|
+
return step({
|
|
6234
|
+
...state,
|
|
6235
|
+
overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
|
|
6236
|
+
});
|
|
6237
|
+
}
|
|
6238
|
+
return step(state, [{ type: "addTask", sessionId: state.sessionId, title }]);
|
|
6022
6239
|
}
|
|
6023
6240
|
function taskCommand(state, token, run) {
|
|
6024
6241
|
return withSession(state, (sessionId) => {
|
|
@@ -6058,7 +6275,9 @@ function createApp(deps) {
|
|
|
6058
6275
|
function dispatch(action) {
|
|
6059
6276
|
if (exited) return;
|
|
6060
6277
|
const result = reduce(state, action);
|
|
6278
|
+
const previousSession = state.sessionId;
|
|
6061
6279
|
state = result.state;
|
|
6280
|
+
if (state.sessionId && state.sessionId !== previousSession) deps.onSessionChange?.(state.sessionId, state);
|
|
6062
6281
|
if (state.exiting) {
|
|
6063
6282
|
exited = true;
|
|
6064
6283
|
clearInterval(spinnerInterval);
|
|
@@ -6090,7 +6309,7 @@ function createApp(deps) {
|
|
|
6090
6309
|
|
|
6091
6310
|
// src/tui/effects.ts
|
|
6092
6311
|
var import_child_process4 = require("child_process");
|
|
6093
|
-
var
|
|
6312
|
+
var import_core21 = require("@ordewell/core");
|
|
6094
6313
|
var import_plan_utils4 = require("@ordewell/core/plan-utils");
|
|
6095
6314
|
var ConversationQueue = class {
|
|
6096
6315
|
tail = Promise.resolve();
|
|
@@ -6261,7 +6480,7 @@ async function perform(effect, deps) {
|
|
|
6261
6480
|
dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
|
|
6262
6481
|
return;
|
|
6263
6482
|
case "setPlanner": {
|
|
6264
|
-
const meta =
|
|
6483
|
+
const meta = import_core21.ALL_PROVIDERS[effect.provider];
|
|
6265
6484
|
if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
|
|
6266
6485
|
const env = { AI_PROVIDER: effect.provider };
|
|
6267
6486
|
const settings = await api.updateSettings({ env });
|
|
@@ -6273,7 +6492,7 @@ async function perform(effect, deps) {
|
|
|
6273
6492
|
});
|
|
6274
6493
|
dispatch({
|
|
6275
6494
|
type: "notice",
|
|
6276
|
-
message: `${(0,
|
|
6495
|
+
message: `${(0, import_core21.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
|
|
6277
6496
|
});
|
|
6278
6497
|
await loadModels(deps);
|
|
6279
6498
|
return;
|
|
@@ -6285,7 +6504,7 @@ async function perform(effect, deps) {
|
|
|
6285
6504
|
dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
|
|
6286
6505
|
return;
|
|
6287
6506
|
case "setApiKey": {
|
|
6288
|
-
const meta =
|
|
6507
|
+
const meta = import_core21.ALL_PROVIDERS[effect.provider];
|
|
6289
6508
|
if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
|
|
6290
6509
|
await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
|
|
6291
6510
|
deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
|
|
@@ -6316,6 +6535,11 @@ async function perform(effect, deps) {
|
|
|
6316
6535
|
dispatch({ type: "notice", message: effect.message });
|
|
6317
6536
|
return;
|
|
6318
6537
|
}
|
|
6538
|
+
case "setMaxParallel":
|
|
6539
|
+
await api.updateSettings({ env: { ORDEWELL_MAX_PARALLEL: String(effect.limit) } });
|
|
6540
|
+
deps.setEnvVar("ORDEWELL_MAX_PARALLEL", String(effect.limit));
|
|
6541
|
+
dispatch({ type: "notice", message: `Up to ${effect.limit} AI task${effect.limit === 1 ? "" : "s"} now run at once.` });
|
|
6542
|
+
return;
|
|
6319
6543
|
case "setAutonomous":
|
|
6320
6544
|
deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
|
|
6321
6545
|
dispatch({ type: "notice", message: `Autonomous mode ${effect.enabled ? "on" : "off"}.` });
|
|
@@ -6352,13 +6576,23 @@ async function perform(effect, deps) {
|
|
|
6352
6576
|
return;
|
|
6353
6577
|
}
|
|
6354
6578
|
case "loadRewindTargets":
|
|
6355
|
-
dispatch({
|
|
6579
|
+
dispatch({
|
|
6580
|
+
type: "rewindTargetsLoaded",
|
|
6581
|
+
targets: await api.rewindTargets(effect.sessionId),
|
|
6582
|
+
sessionId: effect.sessionId,
|
|
6583
|
+
...effect.pick !== void 0 && { pick: effect.pick }
|
|
6584
|
+
});
|
|
6356
6585
|
return;
|
|
6586
|
+
// A rewind is a fork from an earlier point, so the TUI follows it the way
|
|
6587
|
+
// it follows `/fork`; the original keeps its whole conversation. The
|
|
6588
|
+
// rewound message comes back into the input, to edit and send again.
|
|
6357
6589
|
case "rewindConversation": {
|
|
6358
|
-
const
|
|
6359
|
-
dispatch({ type: "
|
|
6360
|
-
dispatch({ type: "
|
|
6361
|
-
dispatch({ type: "
|
|
6590
|
+
const fork = await api.rewindConversation(effect.sessionId, effect.index);
|
|
6591
|
+
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6592
|
+
dispatch({ type: "chatRestored", history: fork.plan.conversationHistory ?? [], sessionId: fork.sessionId });
|
|
6593
|
+
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6594
|
+
dispatch({ type: "inputPrefilled", text: fork.rewoundMessage, sessionId: fork.sessionId });
|
|
6595
|
+
dispatch({ type: "notice", message: `Forked ${effect.sessionId} into ${fork.sessionId} from before that message \u2014 the original is kept (/sessions to go back). The message is ready to edit and resend.` });
|
|
6362
6596
|
return;
|
|
6363
6597
|
}
|
|
6364
6598
|
// The redrawn transcript opens with the summary entry, which is what the
|
|
@@ -6373,15 +6607,17 @@ async function perform(effect, deps) {
|
|
|
6373
6607
|
dispatch({ type: "handoffDiff", diff: await api.reviewRunDiff(effect.sessionId), sessionId: effect.sessionId });
|
|
6374
6608
|
return;
|
|
6375
6609
|
// A conflict or a refusal is an answer, not a fault: either way the user's
|
|
6376
|
-
// tree is exactly as it was, and the words say what to do next.
|
|
6610
|
+
// tree is exactly as it was, and the words say what to do next. A full
|
|
6611
|
+
// merge leaves nothing to hand over — the daemon has cleared the run up.
|
|
6377
6612
|
case "isolationMerge": {
|
|
6378
|
-
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true);
|
|
6613
|
+
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
|
|
6614
|
+
if (ok) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
6379
6615
|
dispatch({ type: ok ? "notice" : "failed", message });
|
|
6380
6616
|
return;
|
|
6381
6617
|
}
|
|
6382
6618
|
case "isolationDiscard":
|
|
6383
6619
|
await api.discardRun(effect.sessionId);
|
|
6384
|
-
dispatch({ type: "
|
|
6620
|
+
dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
6385
6621
|
dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
|
|
6386
6622
|
return;
|
|
6387
6623
|
case "isolationCleanup":
|
|
@@ -6418,7 +6654,7 @@ async function perform(effect, deps) {
|
|
|
6418
6654
|
return;
|
|
6419
6655
|
case "refresh":
|
|
6420
6656
|
await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
|
|
6421
|
-
dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
6657
|
+
if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
6422
6658
|
return;
|
|
6423
6659
|
case "exit":
|
|
6424
6660
|
deps.exit();
|
|
@@ -6434,7 +6670,7 @@ function osc52(text) {
|
|
|
6434
6670
|
function copySelection(deps, text) {
|
|
6435
6671
|
if (!text) return;
|
|
6436
6672
|
const lines = text.split("\n").length;
|
|
6437
|
-
const command = (0,
|
|
6673
|
+
const command = (0, import_core21.clipboardCopyCommand)(deps.hasBin);
|
|
6438
6674
|
if (command) {
|
|
6439
6675
|
try {
|
|
6440
6676
|
(deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
|
|
@@ -6571,12 +6807,12 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
6571
6807
|
return;
|
|
6572
6808
|
}
|
|
6573
6809
|
case "task_started":
|
|
6574
|
-
dispatch({ type: "
|
|
6810
|
+
dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
|
|
6575
6811
|
return;
|
|
6576
6812
|
// The extension shows these in its checkpoint panel; here they are
|
|
6577
6813
|
// transcript lines, which is the TUI's equivalent surface.
|
|
6578
6814
|
case "checkpoint":
|
|
6579
|
-
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0,
|
|
6815
|
+
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core21.truncateCheckpointSummary)(event.summary)}` });
|
|
6580
6816
|
return;
|
|
6581
6817
|
case "approval_decided":
|
|
6582
6818
|
dispatch({ type: "notice", message: describeApprovalDecision2(event) });
|
|
@@ -6632,8 +6868,10 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
6632
6868
|
case "approval_request":
|
|
6633
6869
|
case "approval_settled":
|
|
6634
6870
|
return;
|
|
6635
|
-
// Its asker already has the
|
|
6871
|
+
// Its asker already has the words, from the merge request itself; a
|
|
6872
|
+
// viewer that did not ask still has to drop a run that is gone.
|
|
6636
6873
|
case "isolation_merge":
|
|
6874
|
+
if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
|
|
6637
6875
|
return;
|
|
6638
6876
|
default: {
|
|
6639
6877
|
const unhandled = event;
|
|
@@ -6958,7 +7196,7 @@ async function handleTui(subArgs) {
|
|
|
6958
7196
|
}
|
|
6959
7197
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
6960
7198
|
try {
|
|
6961
|
-
(0,
|
|
7199
|
+
(0, import_core22.assertWorkspaceExists)(workspace);
|
|
6962
7200
|
} catch {
|
|
6963
7201
|
console.error(
|
|
6964
7202
|
`Workspace "${workspace}" does not exist.
|
|
@@ -6967,7 +7205,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6967
7205
|
);
|
|
6968
7206
|
process.exit(1);
|
|
6969
7207
|
}
|
|
6970
|
-
registerSkillCommands((0,
|
|
7208
|
+
registerSkillCommands((0, import_core22.createSkillsService)(workspace).listSkills());
|
|
6971
7209
|
const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
|
|
6972
7210
|
const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
|
|
6973
7211
|
const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
|
|
@@ -6986,7 +7224,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6986
7224
|
conversationQueue,
|
|
6987
7225
|
port,
|
|
6988
7226
|
dispatch: (action) => app.dispatch(action),
|
|
6989
|
-
newSessionId: () => (0,
|
|
7227
|
+
newSessionId: () => (0, import_core22.mintSessionId)(),
|
|
6990
7228
|
setEnvVar: (key, value) => {
|
|
6991
7229
|
writeEnvVar(findEnvFile(), key, value);
|
|
6992
7230
|
process.env[key] = value;
|
|
@@ -7003,7 +7241,16 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
7003
7241
|
},
|
|
7004
7242
|
exit: () => shutdown(0)
|
|
7005
7243
|
}),
|
|
7006
|
-
onExit: () => shutdown(0)
|
|
7244
|
+
onExit: () => shutdown(0),
|
|
7245
|
+
// The same pointer `ordewell plan` leaves, so `ordewell handoff`,
|
|
7246
|
+
// `terminal <n>` and the other one-shot commands act on the session this
|
|
7247
|
+
// TUI is showing instead of saying there is none.
|
|
7248
|
+
onSessionChange: (sessionId, state) => {
|
|
7249
|
+
try {
|
|
7250
|
+
saveLastSession(sessionId, state.goal, state.runners.filter((r) => r.enabled).map((r) => r.id), workspace);
|
|
7251
|
+
} catch {
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7007
7254
|
});
|
|
7008
7255
|
terminal = openTerminal({
|
|
7009
7256
|
mouse: mouseCapture,
|
|
@@ -7049,6 +7296,7 @@ var COMMANDS = {
|
|
|
7049
7296
|
model: handleModel,
|
|
7050
7297
|
planner: handlePlanner,
|
|
7051
7298
|
"planner-effort": handlePlannerEffort,
|
|
7299
|
+
parallel: handleParallel,
|
|
7052
7300
|
key: handleKey,
|
|
7053
7301
|
runners: handleRunners,
|
|
7054
7302
|
auto: handleAuto,
|
|
@@ -7095,10 +7343,12 @@ function cliVersion() {
|
|
|
7095
7343
|
}
|
|
7096
7344
|
|
|
7097
7345
|
// src/main.ts
|
|
7346
|
+
var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
|
|
7098
7347
|
async function main() {
|
|
7099
|
-
(0,
|
|
7348
|
+
(0, import_core23.migrateOldConfigDir)();
|
|
7100
7349
|
loadEnvFile();
|
|
7101
7350
|
const argv = process.argv.slice(2);
|
|
7351
|
+
if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");
|
|
7102
7352
|
const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");
|
|
7103
7353
|
if (command === "--help" || command === "-h") {
|
|
7104
7354
|
printHelp();
|
|
@@ -7108,6 +7358,10 @@ async function main() {
|
|
|
7108
7358
|
console.log(cliVersion());
|
|
7109
7359
|
return;
|
|
7110
7360
|
}
|
|
7361
|
+
const sessionAt = argv.indexOf("--session-id");
|
|
7362
|
+
if (sessionAt > 0 && argv[sessionAt + 1]) {
|
|
7363
|
+
argv[sessionAt + 1] = expandSessionId(argv[sessionAt + 1], flag(argv, "--workspace") || process.cwd());
|
|
7364
|
+
}
|
|
7111
7365
|
const handler = COMMANDS[command];
|
|
7112
7366
|
if (!handler) {
|
|
7113
7367
|
console.error(`Unknown command: ${command}`);
|