@ordewell/cli 0.5.0 → 0.5.1
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 +290 -132
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/main.ts
|
|
27
|
-
var
|
|
27
|
+
var import_core21 = require("@ordewell/core");
|
|
28
28
|
|
|
29
29
|
// src/utils/env.ts
|
|
30
30
|
var import_fs = require("fs");
|
|
@@ -94,7 +94,7 @@ 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
98
|
|
|
99
99
|
Tasks (<id> is an order number or a task ID):
|
|
100
100
|
ordewell add-task --title "..." Add a task to the current plan
|
|
@@ -616,13 +616,17 @@ var ApiClient = class {
|
|
|
616
616
|
}
|
|
617
617
|
return res.data.targets;
|
|
618
618
|
}
|
|
619
|
-
/**
|
|
619
|
+
/**
|
|
620
|
+
* Fork the conversation from just before the user message at `index` (a
|
|
621
|
+
* transcript position) into a session the daemon has already adopted. The
|
|
622
|
+
* original is left as it was; `rewoundMessage` is that message in full.
|
|
623
|
+
*/
|
|
620
624
|
async rewindConversation(sessionId, index) {
|
|
621
625
|
const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
|
|
622
626
|
if (res.status !== 200) {
|
|
623
627
|
throw new Error(res.data?.error || "Rewind failed");
|
|
624
628
|
}
|
|
625
|
-
return res.data.plan;
|
|
629
|
+
return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan, rewoundMessage: res.data.rewoundMessage };
|
|
626
630
|
}
|
|
627
631
|
/**
|
|
628
632
|
* Condense the conversation into a summary. One planner call, so it can take
|
|
@@ -1262,6 +1266,7 @@ var STATE_OF = {
|
|
|
1262
1266
|
active: "active",
|
|
1263
1267
|
merged: "integrated",
|
|
1264
1268
|
conflict: "conflict",
|
|
1269
|
+
repairing: "repairing",
|
|
1265
1270
|
// A failed integration keeps its refs exactly as a failed verdict does; the
|
|
1266
1271
|
// user sees one thing: work that did not land and can be looked at.
|
|
1267
1272
|
kept: "kept",
|
|
@@ -1293,19 +1298,25 @@ function isolationOfPlan(plan) {
|
|
|
1293
1298
|
const state = STATE_OF[String(record.status)];
|
|
1294
1299
|
if (!state) continue;
|
|
1295
1300
|
const changed = changedRepos(record, legacy, state);
|
|
1301
|
+
const repairedFiles = Array.isArray(record.repairedFiles) && record.repairedFiles.length > 0 ? record.repairedFiles.map(String) : void 0;
|
|
1296
1302
|
tasks[taskId] = {
|
|
1297
1303
|
state,
|
|
1298
1304
|
branch: String(record.branch ?? ""),
|
|
1299
1305
|
worktree: String(record.workspace ?? record.worktree ?? ""),
|
|
1300
1306
|
repos: changed,
|
|
1301
|
-
...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {}
|
|
1307
|
+
...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {},
|
|
1308
|
+
...Array.isArray(record.conflictFiles) && record.conflictFiles.length > 0 ? { conflictFiles: record.conflictFiles.map(String) } : {},
|
|
1309
|
+
// No `repair` (attempt/limit): `conflictRepairAttempts` is server config, never
|
|
1310
|
+
// sent in a persisted plan, so it cannot be reconstructed here. A live
|
|
1311
|
+
// `tasksStatus` update fills it in once the stream catches up.
|
|
1312
|
+
...repairedFiles ? { repairedFiles } : {}
|
|
1302
1313
|
};
|
|
1303
1314
|
if (state === "integrated") {
|
|
1304
|
-
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed });
|
|
1315
|
+
landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed, ...repairedFiles ? { repairedFiles } : {} });
|
|
1305
1316
|
}
|
|
1306
1317
|
}
|
|
1307
1318
|
landed.sort((a, b) => a.order - b.order);
|
|
1308
|
-
const entry = ({ taskId, order, title }) => ({ taskId, order, title });
|
|
1319
|
+
const entry = ({ taskId, order, title, repairedFiles }) => ({ taskId, order, title, ...repairedFiles ? { repairedFiles } : {} });
|
|
1309
1320
|
return {
|
|
1310
1321
|
handoff: {
|
|
1311
1322
|
repos: repos.map((repo) => ({ ...repo, landed: landed.filter((t) => t.changed.includes(repo.path)).map(entry) })),
|
|
@@ -1328,6 +1339,16 @@ function reposWithWork(handoff) {
|
|
|
1328
1339
|
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
1329
1340
|
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
1330
1341
|
}
|
|
1342
|
+
function repairedLanded(handoff) {
|
|
1343
|
+
return handoff.landed.filter((t) => t.repairedFiles?.length);
|
|
1344
|
+
}
|
|
1345
|
+
function repairedNotice(handoff) {
|
|
1346
|
+
const repaired = repairedLanded(handoff);
|
|
1347
|
+
if (repaired.length === 0) return "";
|
|
1348
|
+
const named = repaired.map((t) => `${t.title} (${(0, import_core7.capConflictFiles)(t.repairedFiles)})`);
|
|
1349
|
+
const list = named.length === 1 ? named[0] : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
1350
|
+
return ` ${list} landed through ${repaired.length === 1 ? "a conflict repair" : "conflict repairs"}.`;
|
|
1351
|
+
}
|
|
1331
1352
|
function repoResultLines(handoff) {
|
|
1332
1353
|
return handoff.repos.map(({ path, landed }) => {
|
|
1333
1354
|
const n = landed.length;
|
|
@@ -1337,12 +1358,12 @@ function repoResultLines(handoff) {
|
|
|
1337
1358
|
function taskRepoNames(isolation) {
|
|
1338
1359
|
return (isolation?.repos ?? []).filter((r) => r !== ".");
|
|
1339
1360
|
}
|
|
1340
|
-
function mergeOutcome(result, branch, group) {
|
|
1361
|
+
function mergeOutcome(result, branch, group, repaired = []) {
|
|
1341
1362
|
const ok = result.outcome === "merged";
|
|
1342
1363
|
if (!group && result.outcome === "conflict") {
|
|
1343
1364
|
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
1365
|
}
|
|
1345
|
-
const { message } = (0, import_core7.describeMergeResult)(result, branch, group);
|
|
1366
|
+
const { message } = (0, import_core7.describeMergeResult)(result, branch, group, repaired);
|
|
1346
1367
|
return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
|
|
1347
1368
|
}
|
|
1348
1369
|
|
|
@@ -2384,7 +2405,7 @@ async function handleCompact(subArgs, injectedApi) {
|
|
|
2384
2405
|
async function handleRewind(subArgs, injectedApi) {
|
|
2385
2406
|
const arg = positionals(subArgs)[0];
|
|
2386
2407
|
if (arg !== void 0 && !/^\d+$/.test(arg)) fail(REWIND_USAGE);
|
|
2387
|
-
const { api, sessionId } = await adopted(subArgs, injectedApi);
|
|
2408
|
+
const { api, sessionId, workspace } = await adopted(subArgs, injectedApi);
|
|
2388
2409
|
if (arg === void 0) {
|
|
2389
2410
|
let targets;
|
|
2390
2411
|
try {
|
|
@@ -2398,15 +2419,20 @@ async function handleRewind(subArgs, injectedApi) {
|
|
|
2398
2419
|
}
|
|
2399
2420
|
console.log("Your messages, most recent first:\n");
|
|
2400
2421
|
for (const t of [...targets].reverse()) console.log(` ${String(t.index).padStart(4)} ${t.preview}`);
|
|
2401
|
-
console.log("\n `ordewell rewind <n>`
|
|
2422
|
+
console.log("\n `ordewell rewind <n>` forks the conversation from just before message <n>. The original is kept, and the tasks ride along.");
|
|
2402
2423
|
return;
|
|
2403
2424
|
}
|
|
2425
|
+
let fork;
|
|
2404
2426
|
try {
|
|
2405
|
-
await api.rewindConversation(sessionId, Number(arg));
|
|
2427
|
+
fork = await api.rewindConversation(sessionId, Number(arg));
|
|
2406
2428
|
} catch (err) {
|
|
2407
2429
|
fail(`Failed to rewind: ${err.message}`);
|
|
2408
2430
|
}
|
|
2409
|
-
|
|
2431
|
+
saveLastSession(fork.sessionId, fork.goal, fork.plan.runners ?? [], workspace);
|
|
2432
|
+
console.log(`Forked ${sessionId} into ${fork.sessionId} from just before message ${arg} \u2014 the fork is now the current session.`);
|
|
2433
|
+
console.log(` \`ordewell sessions load ${sessionId}\` to go back to the original.`);
|
|
2434
|
+
console.log("\nThe message you rewound to, to resend or edit:\n");
|
|
2435
|
+
console.log(fork.rewoundMessage);
|
|
2410
2436
|
}
|
|
2411
2437
|
|
|
2412
2438
|
// src/commands/handoff.ts
|
|
@@ -2619,12 +2645,12 @@ function runHandoffAction(state, id) {
|
|
|
2619
2645
|
overlay: isRepoGroup(handoff) ? {
|
|
2620
2646
|
kind: "confirm",
|
|
2621
2647
|
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
|
|
2648
|
+
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
2649
|
action: { kind: "merge-run" }
|
|
2624
2650
|
} : {
|
|
2625
2651
|
kind: "confirm",
|
|
2626
2652
|
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
|
|
2653
|
+
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
2654
|
action: { kind: "merge-run" }
|
|
2629
2655
|
}
|
|
2630
2656
|
},
|
|
@@ -2649,7 +2675,14 @@ function confirmedHandoff(state, kind) {
|
|
|
2649
2675
|
const closed = { ...state, overlay: null };
|
|
2650
2676
|
if (!state.sessionId || !state.handoff) return { state: closed, effects: [] };
|
|
2651
2677
|
const { sessionId, handoff } = state;
|
|
2652
|
-
const
|
|
2678
|
+
const repaired = repairedLanded(handoff);
|
|
2679
|
+
const effect = kind === "merge-run" ? {
|
|
2680
|
+
type: "isolationMerge",
|
|
2681
|
+
sessionId,
|
|
2682
|
+
branch: handoffBranch(handoff),
|
|
2683
|
+
...isRepoGroup(handoff) ? { group: true } : {},
|
|
2684
|
+
...repaired.length > 0 ? { repaired } : {}
|
|
2685
|
+
} : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
|
|
2653
2686
|
return { state: closed, effects: [effect] };
|
|
2654
2687
|
}
|
|
2655
2688
|
function showDiff(state, diff) {
|
|
@@ -2714,7 +2747,7 @@ function chooseBlocked(state, choice) {
|
|
|
2714
2747
|
return { state: closed, effects: [{ type: "isolationContinue", sessionId, mode: choice }] };
|
|
2715
2748
|
}
|
|
2716
2749
|
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");
|
|
2750
|
+
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
2751
|
}
|
|
2719
2752
|
function mapTasks(tasks, map) {
|
|
2720
2753
|
return tasks.map((task) => {
|
|
@@ -2787,8 +2820,8 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
|
|
|
2787
2820
|
return;
|
|
2788
2821
|
}
|
|
2789
2822
|
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);
|
|
2823
|
+
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));
|
|
2824
|
+
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
|
|
2792
2825
|
if (ok) console.log(message);
|
|
2793
2826
|
else fail(message);
|
|
2794
2827
|
return;
|
|
@@ -3355,7 +3388,7 @@ Dependencies \xB7 #${task.order} ${task.title}
|
|
|
3355
3388
|
}
|
|
3356
3389
|
|
|
3357
3390
|
// src/tui/index.ts
|
|
3358
|
-
var
|
|
3391
|
+
var import_core20 = require("@ordewell/core");
|
|
3359
3392
|
|
|
3360
3393
|
// src/tui/editor.ts
|
|
3361
3394
|
function emptyEditor() {
|
|
@@ -3525,7 +3558,7 @@ var SLASH_COMMANDS = [
|
|
|
3525
3558
|
{ name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
|
|
3526
3559
|
{ name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
|
|
3527
3560
|
{ 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: "
|
|
3561
|
+
{ 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
3562
|
{ name: "new", usage: "/new", description: "Start a new session", category: "session" },
|
|
3530
3563
|
{ name: "save", usage: "/save", description: "Save the current session", category: "session" },
|
|
3531
3564
|
{ name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
|
|
@@ -3951,12 +3984,9 @@ function chatBodyLines(messages, cols) {
|
|
|
3951
3984
|
}
|
|
3952
3985
|
function chatLayout(state, rows, cols) {
|
|
3953
3986
|
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
3987
|
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) };
|
|
3988
|
+
const lines = !welcome ? body : state.messages.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
|
|
3989
|
+
return { lines, anchor: lines.length > rows ? "bottom" : "top", maxScroll: Math.max(0, lines.length - rows) };
|
|
3960
3990
|
}
|
|
3961
3991
|
function chatScrollMax(state) {
|
|
3962
3992
|
return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
|
|
@@ -4073,33 +4103,69 @@ var STATUS_ICON = {
|
|
|
4073
4103
|
pending: "\xB7"
|
|
4074
4104
|
};
|
|
4075
4105
|
var IDLE_ICON = "~";
|
|
4106
|
+
var FIRST_ROW_LINE = 2;
|
|
4076
4107
|
function planLayout(state, rows, cols) {
|
|
4077
4108
|
const done = state.tasks.filter((t) => t.status === "completed").length;
|
|
4078
4109
|
const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
|
|
4079
|
-
|
|
4110
|
+
const rowSpans = [];
|
|
4080
4111
|
let editorLine;
|
|
4081
4112
|
planRows(state).forEach((row, i) => {
|
|
4082
4113
|
const taskStart = lines.length;
|
|
4083
|
-
if (i === state.selectedTask) selectedLine = taskStart;
|
|
4084
4114
|
const renderedTask = taskLines(state, row, i, cols);
|
|
4085
4115
|
if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
|
|
4086
4116
|
editorLine = taskStart + renderedTask.editorLine;
|
|
4087
4117
|
}
|
|
4088
4118
|
lines.push(...renderedTask.lines);
|
|
4119
|
+
rowSpans.push({ start: taskStart, end: lines.length - 1 });
|
|
4089
4120
|
});
|
|
4090
|
-
const
|
|
4091
|
-
const
|
|
4092
|
-
return { lines,
|
|
4121
|
+
const selected = rowSpans[state.selectedTask] ?? { start: FIRST_ROW_LINE, end: FIRST_ROW_LINE };
|
|
4122
|
+
const anchor = editorLine === void 0 ? selected : { start: editorLine, end: editorLine };
|
|
4123
|
+
return { lines, rowSpans, anchor, rows, maxScroll: Math.max(0, lines.length - rows) };
|
|
4124
|
+
}
|
|
4125
|
+
var clampOffset = (layout, offset) => Math.max(0, Math.min(layout.maxScroll, offset));
|
|
4126
|
+
function revealOffset(layout, offset, span) {
|
|
4127
|
+
const at = clampOffset(layout, offset);
|
|
4128
|
+
const bodyHeight = layout.rows - 1;
|
|
4129
|
+
const first = span.start <= FIRST_ROW_LINE ? 0 : span.start - 1;
|
|
4130
|
+
if (span.start < 1 + at || span.end - span.start + 1 > bodyHeight) return clampOffset(layout, first);
|
|
4131
|
+
if (span.end > at + layout.rows - 1) return clampOffset(layout, span.end - layout.rows + 1);
|
|
4132
|
+
return at;
|
|
4093
4133
|
}
|
|
4094
4134
|
function planOffset(layout, planScroll) {
|
|
4095
|
-
if (planScroll === null) return layout.
|
|
4096
|
-
return
|
|
4135
|
+
if (planScroll === null) return revealOffset(layout, 0, layout.anchor);
|
|
4136
|
+
return clampOffset(layout, planScroll);
|
|
4137
|
+
}
|
|
4138
|
+
function topRowInView(layout, offset) {
|
|
4139
|
+
const top = 1 + offset;
|
|
4140
|
+
const whole = layout.rowSpans.findIndex((span) => span.start >= top && span.end <= offset + layout.rows - 1);
|
|
4141
|
+
if (whole >= 0) return whole;
|
|
4142
|
+
const covering = layout.rowSpans.findIndex((span) => span.end >= top);
|
|
4143
|
+
return covering >= 0 ? covering : layout.rowSpans.length - 1;
|
|
4144
|
+
}
|
|
4145
|
+
function bottomRowInView(layout, offset) {
|
|
4146
|
+
const bottom = offset + layout.rows - 1;
|
|
4147
|
+
const spans = layout.rowSpans;
|
|
4148
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
4149
|
+
if (spans[i].start >= 1 + offset && spans[i].end <= bottom) return i;
|
|
4150
|
+
}
|
|
4151
|
+
for (let i = spans.length - 1; i >= 0; i--) {
|
|
4152
|
+
if (spans[i].start <= bottom) return i;
|
|
4153
|
+
}
|
|
4154
|
+
return 0;
|
|
4155
|
+
}
|
|
4156
|
+
function rowNearestView(layout, offset, row) {
|
|
4157
|
+
const span = layout.rowSpans[row];
|
|
4158
|
+
if (!span) return row;
|
|
4159
|
+
if (span.end < 1 + offset) return topRowInView(layout, offset);
|
|
4160
|
+
if (span.start > offset + layout.rows - 1) return bottomRowInView(layout, offset);
|
|
4161
|
+
return row;
|
|
4097
4162
|
}
|
|
4098
4163
|
function planScrollExtent(state) {
|
|
4099
4164
|
const cols = planPaneWidth(state);
|
|
4100
|
-
if (cols === 0)
|
|
4101
|
-
|
|
4102
|
-
|
|
4165
|
+
if (cols === 0) {
|
|
4166
|
+
return { lines: [], rowSpans: [], anchor: { start: FIRST_ROW_LINE, end: FIRST_ROW_LINE }, rows: 0, maxScroll: 0 };
|
|
4167
|
+
}
|
|
4168
|
+
return planLayout(state, bodyRows(state), cols);
|
|
4103
4169
|
}
|
|
4104
4170
|
function taskLines(state, row, index, cols) {
|
|
4105
4171
|
const { task, parent } = row;
|
|
@@ -4138,7 +4204,16 @@ function taskLines(state, row, index, cols) {
|
|
|
4138
4204
|
if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
|
|
4139
4205
|
if (task.isolation?.state === "conflict") {
|
|
4140
4206
|
const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
|
|
4141
|
-
|
|
4207
|
+
const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)})` : "";
|
|
4208
|
+
lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
|
|
4209
|
+
}
|
|
4210
|
+
if (task.isolation?.state === "repairing") {
|
|
4211
|
+
const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)}` : "";
|
|
4212
|
+
const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
|
|
4213
|
+
lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
|
|
4214
|
+
}
|
|
4215
|
+
if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
|
|
4216
|
+
lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core16.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
|
|
4142
4217
|
}
|
|
4143
4218
|
let editorLine;
|
|
4144
4219
|
if (expanded) {
|
|
@@ -4223,6 +4298,7 @@ function helpScrollMax(state) {
|
|
|
4223
4298
|
}
|
|
4224
4299
|
|
|
4225
4300
|
// src/tui/render.ts
|
|
4301
|
+
var import_core17 = require("@ordewell/core");
|
|
4226
4302
|
function render(state) {
|
|
4227
4303
|
const { rows, cols } = state;
|
|
4228
4304
|
const rowsForBody = bodyRows(state);
|
|
@@ -4321,8 +4397,7 @@ function renderSkills(state, cols) {
|
|
|
4321
4397
|
return truncate([...badges, auto].join(" "), cols);
|
|
4322
4398
|
}
|
|
4323
4399
|
function scrolledBackMark(state) {
|
|
4324
|
-
|
|
4325
|
-
return state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
|
|
4400
|
+
return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
|
|
4326
4401
|
}
|
|
4327
4402
|
function renderStatus(state, cols) {
|
|
4328
4403
|
const mark = scrolledBackMark(state);
|
|
@@ -4462,14 +4537,7 @@ function renderOverlay(state, rows, cols) {
|
|
|
4462
4537
|
cols
|
|
4463
4538
|
);
|
|
4464
4539
|
}
|
|
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
|
-
}
|
|
4540
|
+
if (overlay.kind === "confirm") return renderConfirm(overlay, rows, cols);
|
|
4473
4541
|
if (overlay.kind === "prompt") {
|
|
4474
4542
|
const secret = overlay.action.kind === "api-key";
|
|
4475
4543
|
const shown = secret ? "\u2022".repeat(overlay.value.length) : overlay.value;
|
|
@@ -4493,7 +4561,10 @@ function renderHandoff(state, overlay, rows, cols) {
|
|
|
4493
4561
|
const more = scroll + room < lines.length ? "\u2191\u2193 pgup/pgdn scroll \xB7 " : "";
|
|
4494
4562
|
return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
|
|
4495
4563
|
}
|
|
4496
|
-
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) =>
|
|
4564
|
+
const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
|
|
4565
|
+
const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core17.capConflictFiles)(task.repairedFiles)})`) : "";
|
|
4566
|
+
return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
|
|
4567
|
+
});
|
|
4497
4568
|
const actions = handoffActions(handoff).map((action, index) => {
|
|
4498
4569
|
const active = index === overlay.index;
|
|
4499
4570
|
const caret = active ? style.cyan("\u276F") : " ";
|
|
@@ -4553,6 +4624,24 @@ function renderHelp(scroll, rows, cols) {
|
|
|
4553
4624
|
const footer = style.grey(offset < maxScroll ? "\u2191\u2193 scroll \xB7 any other key closes" : "any key closes");
|
|
4554
4625
|
return frame("Commands", [...lines.slice(offset, offset + room), footer], rows, cols);
|
|
4555
4626
|
}
|
|
4627
|
+
var QUOTE_MAX_LINES = 8;
|
|
4628
|
+
function renderConfirm(overlay, rows, cols) {
|
|
4629
|
+
const { choice, quote, note } = overlay;
|
|
4630
|
+
const inner = Math.max(1, cols - 3);
|
|
4631
|
+
const options = (choice?.options ?? []).map((o, i) => `${i === choice?.index ? style.cyan("\u276F") : " "} ${i + 1}. ${o.label}`);
|
|
4632
|
+
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");
|
|
4633
|
+
const noteLines = note ? ["", ...note.split("\n")] : [];
|
|
4634
|
+
const rest = [...noteLines, ...options.length > 0 ? ["", ...options] : [], "", hint];
|
|
4635
|
+
const quoted = [];
|
|
4636
|
+
if (quote !== void 0) {
|
|
4637
|
+
const fixed = 1 + wrap(overlay.message, inner).length + 1 + 1 + rest.flatMap((l) => wrap(l, inner)).length;
|
|
4638
|
+
const room = Math.max(2, Math.min(QUOTE_MAX_LINES, rows - fixed));
|
|
4639
|
+
const wrapped = wrap(quote, inner - 2);
|
|
4640
|
+
const shown = wrapped.length > room ? [...wrapped.slice(0, room - 1), "\u2026"] : wrapped;
|
|
4641
|
+
quoted.push("", ...shown.map((l) => `${style.grey("\u2502")} ${l}`));
|
|
4642
|
+
}
|
|
4643
|
+
return frame(overlay.title, [overlay.message, ...quoted, ...rest], rows, cols);
|
|
4644
|
+
}
|
|
4556
4645
|
function frame(title, body, rows, cols) {
|
|
4557
4646
|
const inner = Math.max(1, cols - 2);
|
|
4558
4647
|
const lines = [
|
|
@@ -4563,9 +4652,9 @@ function frame(title, body, rows, cols) {
|
|
|
4563
4652
|
}
|
|
4564
4653
|
|
|
4565
4654
|
// src/tui/reducer.ts
|
|
4566
|
-
var
|
|
4655
|
+
var import_core18 = require("@ordewell/core");
|
|
4567
4656
|
var import_plan_utils3 = require("@ordewell/core/plan-utils");
|
|
4568
|
-
var KNOWN_PROVIDERS2 = Object.keys(
|
|
4657
|
+
var KNOWN_PROVIDERS2 = Object.keys(import_core18.ALL_PROVIDERS);
|
|
4569
4658
|
var DEFAULT_EFFORT = "__runner_default__";
|
|
4570
4659
|
function step(state, effects = []) {
|
|
4571
4660
|
return { state, effects };
|
|
@@ -4653,7 +4742,8 @@ function reduce(state, action) {
|
|
|
4653
4742
|
expandedTaskId,
|
|
4654
4743
|
taskEditor: expandedTaskId !== null ? state.taskEditor : null
|
|
4655
4744
|
};
|
|
4656
|
-
|
|
4745
|
+
const selectedTask = clampSelection(state.selectedTask, planRows(next).length);
|
|
4746
|
+
return step(settlePlan({ ...next, selectedTask, focus: tasks.length === 0 ? "chat" : state.focus }));
|
|
4657
4747
|
}
|
|
4658
4748
|
case "plannerMessage": {
|
|
4659
4749
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -4746,7 +4836,7 @@ function reduce(state, action) {
|
|
|
4746
4836
|
if (stale(state, action.sessionId)) return step(state);
|
|
4747
4837
|
return step(showDiff(state, action.diff));
|
|
4748
4838
|
}
|
|
4749
|
-
case "
|
|
4839
|
+
case "runCleared": {
|
|
4750
4840
|
if (stale(state, action.sessionId)) return step(state);
|
|
4751
4841
|
const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
|
|
4752
4842
|
return step({ ...clearIsolation(state), overlay });
|
|
@@ -4787,6 +4877,7 @@ function reduce(state, action) {
|
|
|
4787
4877
|
return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
|
|
4788
4878
|
case "rewindTargetsLoaded":
|
|
4789
4879
|
if (stale(state, action.sessionId)) return step(state);
|
|
4880
|
+
if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
|
|
4790
4881
|
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
4791
4882
|
// A fork holds no run, whatever the session it came from was doing — the
|
|
4792
4883
|
// original keeps its run, and its events are stale from here on.
|
|
@@ -4802,6 +4893,15 @@ function reduce(state, action) {
|
|
|
4802
4893
|
pendingApprovals: [],
|
|
4803
4894
|
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
4804
4895
|
});
|
|
4896
|
+
// The rewound message comes back as a draft to edit and resend, so it lands
|
|
4897
|
+
// as if just typed: history is not being browsed, and nothing is parked.
|
|
4898
|
+
case "inputPrefilled":
|
|
4899
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
4900
|
+
return step({
|
|
4901
|
+
...state,
|
|
4902
|
+
focus: "chat",
|
|
4903
|
+
editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
4904
|
+
});
|
|
4805
4905
|
case "runnersLoaded":
|
|
4806
4906
|
return step(refillPicker({
|
|
4807
4907
|
...state,
|
|
@@ -4827,11 +4927,7 @@ function reduce(state, action) {
|
|
|
4827
4927
|
return step(say(state, "system", action.message));
|
|
4828
4928
|
case "resize": {
|
|
4829
4929
|
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
|
-
});
|
|
4930
|
+
return step(settlePlan({ ...resized, scroll: clamp(resized.scroll, chatScrollMax(resized)) }));
|
|
4835
4931
|
}
|
|
4836
4932
|
// Whether there is anything to animate is the app loop's call (it owns the
|
|
4837
4933
|
// timer); a tick that arrives simply advances the frame.
|
|
@@ -4906,11 +5002,11 @@ function modelDetail(m) {
|
|
|
4906
5002
|
function providerErrorHint(state) {
|
|
4907
5003
|
const failed = Object.keys(state.providerErrors ?? {});
|
|
4908
5004
|
if (failed.length === 0) return void 0;
|
|
4909
|
-
const names = failed.map((p) =>
|
|
5005
|
+
const names = failed.map((p) => import_core18.ALL_PROVIDERS[p]?.label ?? p);
|
|
4910
5006
|
return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
|
|
4911
5007
|
}
|
|
4912
5008
|
function plannerModelItems(state) {
|
|
4913
|
-
const runner = (0,
|
|
5009
|
+
const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
|
|
4914
5010
|
if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
|
|
4915
5011
|
const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
|
|
4916
5012
|
id: m.id,
|
|
@@ -4924,7 +5020,7 @@ function plannerModelItems(state) {
|
|
|
4924
5020
|
return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
|
|
4925
5021
|
}
|
|
4926
5022
|
function plannerEffortItems(state) {
|
|
4927
|
-
const runner = (0,
|
|
5023
|
+
const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
|
|
4928
5024
|
if (!runner) {
|
|
4929
5025
|
return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
|
|
4930
5026
|
}
|
|
@@ -5049,7 +5145,7 @@ function pointerPane(state, key) {
|
|
|
5049
5145
|
}
|
|
5050
5146
|
function scrollPointed(state, key) {
|
|
5051
5147
|
const delta = key.name === "scrollup" ? WHEEL_NOTCH : -WHEEL_NOTCH;
|
|
5052
|
-
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta) : scrollChat(state, delta);
|
|
5148
|
+
return pointerPane(state, key) === "plan" ? scrollPlan(state, delta, false) : scrollChat(state, delta);
|
|
5053
5149
|
}
|
|
5054
5150
|
var isMouseSelect = (key) => key.name === "mousedown" || key.name === "mousedrag" || key.name === "mouseup";
|
|
5055
5151
|
function selectionCell(state, pane, key) {
|
|
@@ -5082,10 +5178,23 @@ function clamp(value, max) {
|
|
|
5082
5178
|
function scrollChat(state, delta) {
|
|
5083
5179
|
return step({ ...state, scroll: clamp(state.scroll + delta, chatScrollMax(state)) });
|
|
5084
5180
|
}
|
|
5085
|
-
function
|
|
5086
|
-
|
|
5087
|
-
const
|
|
5088
|
-
return
|
|
5181
|
+
function settlePlan(state) {
|
|
5182
|
+
if (planPaneWidth(state) === 0) return state;
|
|
5183
|
+
const layout = planScrollExtent(state);
|
|
5184
|
+
return { ...state, planScroll: revealOffset(layout, planOffset(layout, state.planScroll), layout.anchor) };
|
|
5185
|
+
}
|
|
5186
|
+
function scrollPlan(state, delta, paged) {
|
|
5187
|
+
const layout = planScrollExtent(state);
|
|
5188
|
+
const from = planOffset(layout, state.planScroll);
|
|
5189
|
+
const to = clamp(from - delta, layout.maxScroll);
|
|
5190
|
+
const scrolled = { ...state, planScroll: to };
|
|
5191
|
+
if (state.taskEditor || layout.rowSpans.length === 0) return step(scrolled);
|
|
5192
|
+
const back = delta > 0;
|
|
5193
|
+
if (!paged) {
|
|
5194
|
+
return step({ ...scrolled, selectedTask: rowNearestView(layout, to, state.selectedTask) });
|
|
5195
|
+
}
|
|
5196
|
+
if (to === from) return step({ ...scrolled, selectedTask: back ? 0 : layout.rowSpans.length - 1 });
|
|
5197
|
+
return step({ ...scrolled, selectedTask: back ? bottomRowInView(layout, to) : topRowInView(layout, to) });
|
|
5089
5198
|
}
|
|
5090
5199
|
var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
|
|
5091
5200
|
function stopPlanning(state, sessionId) {
|
|
@@ -5115,7 +5224,10 @@ function handleKey2(state, key) {
|
|
|
5115
5224
|
return step({ ...state, editor: { ...state.editor, text, cursor: token.start + completed.length } });
|
|
5116
5225
|
}
|
|
5117
5226
|
}
|
|
5118
|
-
if (key.name === "tab")
|
|
5227
|
+
if (key.name === "tab") {
|
|
5228
|
+
if (state.focus === "chat" && state.tasks.length === 0) return step(state);
|
|
5229
|
+
return step({ ...state, focus: state.focus === "chat" ? "plan" : "chat" });
|
|
5230
|
+
}
|
|
5119
5231
|
if (state.focus === "plan") return handlePlanKey(state, key);
|
|
5120
5232
|
if (key.name === "enter") return submit(state);
|
|
5121
5233
|
if (key.name === "escape") return step({ ...state, editor: { ...state.editor, text: "", cursor: 0 } });
|
|
@@ -5147,35 +5259,32 @@ function handlePlanKey(state, key) {
|
|
|
5147
5259
|
const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
|
|
5148
5260
|
if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
|
|
5149
5261
|
if (key.name === "escape") {
|
|
5150
|
-
if (state.expandedTaskId) return step({ ...state, expandedTaskId: null
|
|
5262
|
+
if (state.expandedTaskId) return step(settlePlan({ ...state, expandedTaskId: null }));
|
|
5151
5263
|
return step({ ...state, focus: "chat" });
|
|
5152
5264
|
}
|
|
5153
5265
|
if (key.name === "up") {
|
|
5154
|
-
return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1)
|
|
5266
|
+
return step(settlePlan({ ...state, selectedTask: Math.max(0, state.selectedTask - 1) }));
|
|
5155
5267
|
}
|
|
5156
5268
|
if (key.name === "down") {
|
|
5157
|
-
return step({
|
|
5158
|
-
...state,
|
|
5159
|
-
selectedTask: Math.min(planRows(state).length - 1, state.selectedTask + 1),
|
|
5160
|
-
planScroll: null
|
|
5161
|
-
});
|
|
5269
|
+
return step(settlePlan({ ...state, selectedTask: clampSelection(state.selectedTask + 1, planRows(state).length) }));
|
|
5162
5270
|
}
|
|
5163
5271
|
const scroll = scrollDelta(key, state);
|
|
5164
|
-
if (scroll !== null) return scrollPlan(state, scroll);
|
|
5272
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5273
|
+
if (key.name === "char" && key.char === "a") return addTask(state, "");
|
|
5165
5274
|
const row = selectedPlanRow(state);
|
|
5166
5275
|
if (!row) return step(state);
|
|
5167
5276
|
const task = row.task;
|
|
5168
5277
|
if (key.name === "enter" || key.name === "right") {
|
|
5169
5278
|
const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
|
|
5170
5279
|
if (hasSubtasks && state.expandedTaskId !== task.id) {
|
|
5171
|
-
return step({ ...state, expandedTaskId: task.id, taskEditor: null
|
|
5280
|
+
return step(settlePlan({ ...state, expandedTaskId: task.id, taskEditor: null }));
|
|
5172
5281
|
}
|
|
5173
5282
|
const text = task.prompt ?? task.description ?? task.title;
|
|
5174
|
-
return step({
|
|
5283
|
+
return step(settlePlan({
|
|
5175
5284
|
...state,
|
|
5176
5285
|
expandedTaskId: task.id,
|
|
5177
5286
|
taskEditor: { ...emptyEditor(), text, cursor: text.length }
|
|
5178
|
-
});
|
|
5287
|
+
}));
|
|
5179
5288
|
}
|
|
5180
5289
|
if (!state.sessionId || key.name !== "char") return step(state);
|
|
5181
5290
|
if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
|
|
@@ -5184,7 +5293,6 @@ function handlePlanKey(state, key) {
|
|
|
5184
5293
|
return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
|
|
5185
5294
|
}
|
|
5186
5295
|
if (key.char === "d") return confirmRemoveTask(state, task);
|
|
5187
|
-
if (key.char === "a") return addTask(state, "");
|
|
5188
5296
|
if (key.char === "t") {
|
|
5189
5297
|
return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
|
|
5190
5298
|
}
|
|
@@ -5200,13 +5308,13 @@ function handlePlanKey(state, key) {
|
|
|
5200
5308
|
}
|
|
5201
5309
|
function handleTaskEditKey(state, task, editor, key) {
|
|
5202
5310
|
if (key.name === "enter") return commitTaskEdit(state, task, editor);
|
|
5203
|
-
if (key.name === "escape") return step({ ...state, expandedTaskId: null, taskEditor: null });
|
|
5311
|
+
if (key.name === "escape") return step(settlePlan({ ...state, expandedTaskId: null, taskEditor: null }));
|
|
5204
5312
|
const scroll = scrollDelta(key, state);
|
|
5205
|
-
if (scroll !== null) return scrollPlan(state, scroll);
|
|
5206
|
-
return step({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) });
|
|
5313
|
+
if (scroll !== null) return scrollPlan(state, scroll, true);
|
|
5314
|
+
return step(settlePlan({ ...state, taskEditor: applyKey(editor, key, taskEditorRoom(state)) }));
|
|
5207
5315
|
}
|
|
5208
5316
|
function commitTaskEdit(state, task, editor) {
|
|
5209
|
-
const collapsed = { ...state, expandedTaskId: null, taskEditor: null };
|
|
5317
|
+
const collapsed = settlePlan({ ...state, expandedTaskId: null, taskEditor: null });
|
|
5210
5318
|
const prompt = editor.text.trim();
|
|
5211
5319
|
const original = task.prompt ?? task.description ?? task.title;
|
|
5212
5320
|
if (!prompt || prompt === original || !state.sessionId) return step(collapsed);
|
|
@@ -5261,7 +5369,18 @@ function handleOverlayKey(state, overlay, key) {
|
|
|
5261
5369
|
}
|
|
5262
5370
|
function handleConfirmKey(state, overlay, key) {
|
|
5263
5371
|
if (key.name === "escape") return step({ ...state, overlay: null });
|
|
5264
|
-
|
|
5372
|
+
const { choice } = overlay;
|
|
5373
|
+
if (!choice) return key.name === "enter" ? runConfirm(state, overlay) : step(state);
|
|
5374
|
+
if (key.name === "up" || key.name === "down") {
|
|
5375
|
+
const index = clamp(choice.index + (key.name === "down" ? 1 : -1), choice.options.length - 1);
|
|
5376
|
+
return step({ ...state, overlay: { ...overlay, choice: { ...choice, index } } });
|
|
5377
|
+
}
|
|
5378
|
+
const chosen = key.name === "enter" ? choice.index : key.name === "char" ? Number(key.char) - 1 : -1;
|
|
5379
|
+
const option = choice.options[chosen];
|
|
5380
|
+
if (!option) return step(state);
|
|
5381
|
+
return option.confirms ? runConfirm(state, overlay) : step({ ...state, overlay: null });
|
|
5382
|
+
}
|
|
5383
|
+
function runConfirm(state, overlay) {
|
|
5265
5384
|
const closed = { ...state, overlay: null };
|
|
5266
5385
|
if (overlay.action.kind === "new-session") return newSession(closed);
|
|
5267
5386
|
if (overlay.action.kind === "remove-task") {
|
|
@@ -5274,7 +5393,8 @@ function handleConfirmKey(state, overlay, key) {
|
|
|
5274
5393
|
if (overlay.action.kind === "init-workspace") {
|
|
5275
5394
|
return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
|
|
5276
5395
|
}
|
|
5277
|
-
|
|
5396
|
+
const { index } = overlay.action;
|
|
5397
|
+
return withIdlePlanner(closed, (sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index }]));
|
|
5278
5398
|
}
|
|
5279
5399
|
function handleHelpKey(state, scroll, key) {
|
|
5280
5400
|
const page = pageNotch(bodyRows(state));
|
|
@@ -5298,7 +5418,7 @@ function handlePromptKey(state, overlay, key) {
|
|
|
5298
5418
|
if (overlay.action.kind === "api-key") {
|
|
5299
5419
|
return step(closed, [{ type: "setApiKey", provider: overlay.action.provider, key: value2 }]);
|
|
5300
5420
|
}
|
|
5301
|
-
if (!state.sessionId) return fail3(closed, "No active plan to add a task to
|
|
5421
|
+
if (!state.sessionId) return fail3(closed, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
5302
5422
|
return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
|
|
5303
5423
|
}
|
|
5304
5424
|
const value = editText(overlay.value, key);
|
|
@@ -5371,10 +5491,7 @@ function choose(state, picker2, item) {
|
|
|
5371
5491
|
case "load-session":
|
|
5372
5492
|
return step(closed, [{ type: "loadSession", sessionId: item.id }]);
|
|
5373
5493
|
case "rewind":
|
|
5374
|
-
return
|
|
5375
|
-
closed,
|
|
5376
|
-
(sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index: Number(item.id) }])
|
|
5377
|
-
);
|
|
5494
|
+
return pickRewindTarget(state, Number(item.id));
|
|
5378
5495
|
case "delete-session":
|
|
5379
5496
|
return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
|
|
5380
5497
|
case "set-key":
|
|
@@ -5455,7 +5572,7 @@ function runCommand(state, { name, args }) {
|
|
|
5455
5572
|
case "quit":
|
|
5456
5573
|
return step({ ...state, exiting: true }, [{ type: "exit" }]);
|
|
5457
5574
|
case "refresh":
|
|
5458
|
-
return step(state, [{ type: "refresh" }]);
|
|
5575
|
+
return step(state, [{ type: "refresh", announce: true }]);
|
|
5459
5576
|
case "run":
|
|
5460
5577
|
return withSession(state, (sessionId) => step(state, [{ type: "execute", sessionId }]));
|
|
5461
5578
|
case "approve":
|
|
@@ -5571,12 +5688,34 @@ function rewind(state, arg) {
|
|
|
5571
5688
|
return withIdlePlanner(state, (sessionId) => {
|
|
5572
5689
|
if (arg === void 0) {
|
|
5573
5690
|
return step(
|
|
5574
|
-
{ ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "
|
|
5691
|
+
{ ...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
5692
|
[{ type: "loadRewindTargets", sessionId }]
|
|
5576
5693
|
);
|
|
5577
5694
|
}
|
|
5578
5695
|
if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
|
|
5579
|
-
return step(state, [{ type: "
|
|
5696
|
+
return step(state, [{ type: "loadRewindTargets", sessionId, pick: Number(arg) }]);
|
|
5697
|
+
});
|
|
5698
|
+
}
|
|
5699
|
+
var REWIND_OPTIONS = [
|
|
5700
|
+
{ label: "Restore Conversation", confirms: true },
|
|
5701
|
+
{ label: "Never mind", confirms: false }
|
|
5702
|
+
];
|
|
5703
|
+
function pickRewindTarget(state, index) {
|
|
5704
|
+
return withIdlePlanner({ ...state, overlay: null }, () => {
|
|
5705
|
+
const target = state.rewindTargets?.find((t) => t.index === index);
|
|
5706
|
+
if (!target) return fail3({ ...state, overlay: null }, `No message ${index} to rewind to \u2014 /rewind alone lists them.`);
|
|
5707
|
+
return step({
|
|
5708
|
+
...state,
|
|
5709
|
+
overlay: {
|
|
5710
|
+
kind: "confirm",
|
|
5711
|
+
title: "Rewind",
|
|
5712
|
+
message: "Confirm you want to restore to the point before you sent this message:",
|
|
5713
|
+
quote: target.content,
|
|
5714
|
+
note: "The conversation will be forked.\nThe code will be unchanged.",
|
|
5715
|
+
action: { kind: "rewind", index },
|
|
5716
|
+
choice: { options: REWIND_OPTIONS, index: 0 }
|
|
5717
|
+
}
|
|
5718
|
+
});
|
|
5580
5719
|
});
|
|
5581
5720
|
}
|
|
5582
5721
|
function resolveToggle(arg, current) {
|
|
@@ -5618,6 +5757,7 @@ function newSession(state) {
|
|
|
5618
5757
|
expandedTaskId: null,
|
|
5619
5758
|
taskEditor: null,
|
|
5620
5759
|
scroll: 0,
|
|
5760
|
+
planScroll: null,
|
|
5621
5761
|
status: "idle",
|
|
5622
5762
|
busyLabel: "",
|
|
5623
5763
|
thinkingLine: "",
|
|
@@ -5629,7 +5769,7 @@ function newSession(state) {
|
|
|
5629
5769
|
}
|
|
5630
5770
|
function setModel(state, args) {
|
|
5631
5771
|
if (args[0] === "set" && args[1]) {
|
|
5632
|
-
const runner = (0,
|
|
5772
|
+
const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
|
|
5633
5773
|
const known = runner ? modelsForRunner(state.models, runner) : [];
|
|
5634
5774
|
if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
|
|
5635
5775
|
return fail3(state, `${args[1]} was not discovered for ${runner}.`);
|
|
@@ -5651,21 +5791,21 @@ function setModel(state, args) {
|
|
|
5651
5791
|
}
|
|
5652
5792
|
function plannerItems(state) {
|
|
5653
5793
|
const installed = new Set(state.runners.map((r) => r.id));
|
|
5654
|
-
const agents =
|
|
5655
|
-
const runner = (0,
|
|
5794
|
+
const agents = import_core18.CLI_PROVIDERS.map((id) => {
|
|
5795
|
+
const runner = (0, import_core18.runnerForProvider)(id);
|
|
5656
5796
|
const ready = installed.has(runner);
|
|
5657
5797
|
return {
|
|
5658
5798
|
id,
|
|
5659
|
-
label:
|
|
5799
|
+
label: import_core18.ALL_PROVIDERS[id].label,
|
|
5660
5800
|
detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
|
|
5661
5801
|
selected: state.plannerProvider === id,
|
|
5662
5802
|
disabled: !ready
|
|
5663
5803
|
};
|
|
5664
5804
|
});
|
|
5665
|
-
const vendors =
|
|
5805
|
+
const vendors = import_core18.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
|
|
5666
5806
|
id,
|
|
5667
|
-
label:
|
|
5668
|
-
detail:
|
|
5807
|
+
label: import_core18.ALL_PROVIDERS[id].label,
|
|
5808
|
+
detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5669
5809
|
selected: state.plannerProvider === id
|
|
5670
5810
|
}));
|
|
5671
5811
|
return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
|
|
@@ -5723,16 +5863,16 @@ function setKey(state, args) {
|
|
|
5723
5863
|
}
|
|
5724
5864
|
return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
|
|
5725
5865
|
}
|
|
5726
|
-
const items =
|
|
5866
|
+
const items = import_core18.PROVIDER_PRIORITY.map((id) => ({
|
|
5727
5867
|
id,
|
|
5728
|
-
label:
|
|
5729
|
-
detail:
|
|
5868
|
+
label: import_core18.ALL_PROVIDERS[id].label,
|
|
5869
|
+
detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
|
|
5730
5870
|
selected: state.configuredProviders.includes(id)
|
|
5731
5871
|
}));
|
|
5732
5872
|
return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
|
|
5733
5873
|
}
|
|
5734
5874
|
function keyPrompt(provider) {
|
|
5735
|
-
const meta =
|
|
5875
|
+
const meta = import_core18.ALL_PROVIDERS[provider];
|
|
5736
5876
|
return {
|
|
5737
5877
|
kind: "prompt",
|
|
5738
5878
|
title: `${meta.label} API key`,
|
|
@@ -6009,16 +6149,20 @@ function taskEffortCommand(state, args) {
|
|
|
6009
6149
|
return assignTaskEffort(state, sessionId, task, value);
|
|
6010
6150
|
});
|
|
6011
6151
|
}
|
|
6152
|
+
function clampSelection(index, rows) {
|
|
6153
|
+
return Math.max(0, Math.min(index, rows - 1));
|
|
6154
|
+
}
|
|
6012
6155
|
function addTask(state, title) {
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
}
|
|
6156
|
+
if (!state.sessionId) {
|
|
6157
|
+
return fail3(state, "No active plan to add a task to \u2014 describe a goal first, then /add-task <title>.");
|
|
6158
|
+
}
|
|
6159
|
+
if (!title) {
|
|
6160
|
+
return step({
|
|
6161
|
+
...state,
|
|
6162
|
+
overlay: { kind: "prompt", title: "New task title", value: "", action: { kind: "add-task" } }
|
|
6163
|
+
});
|
|
6164
|
+
}
|
|
6165
|
+
return step(state, [{ type: "addTask", sessionId: state.sessionId, title }]);
|
|
6022
6166
|
}
|
|
6023
6167
|
function taskCommand(state, token, run) {
|
|
6024
6168
|
return withSession(state, (sessionId) => {
|
|
@@ -6090,7 +6234,7 @@ function createApp(deps) {
|
|
|
6090
6234
|
|
|
6091
6235
|
// src/tui/effects.ts
|
|
6092
6236
|
var import_child_process4 = require("child_process");
|
|
6093
|
-
var
|
|
6237
|
+
var import_core19 = require("@ordewell/core");
|
|
6094
6238
|
var import_plan_utils4 = require("@ordewell/core/plan-utils");
|
|
6095
6239
|
var ConversationQueue = class {
|
|
6096
6240
|
tail = Promise.resolve();
|
|
@@ -6261,7 +6405,7 @@ async function perform(effect, deps) {
|
|
|
6261
6405
|
dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
|
|
6262
6406
|
return;
|
|
6263
6407
|
case "setPlanner": {
|
|
6264
|
-
const meta =
|
|
6408
|
+
const meta = import_core19.ALL_PROVIDERS[effect.provider];
|
|
6265
6409
|
if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
|
|
6266
6410
|
const env = { AI_PROVIDER: effect.provider };
|
|
6267
6411
|
const settings = await api.updateSettings({ env });
|
|
@@ -6273,7 +6417,7 @@ async function perform(effect, deps) {
|
|
|
6273
6417
|
});
|
|
6274
6418
|
dispatch({
|
|
6275
6419
|
type: "notice",
|
|
6276
|
-
message: `${(0,
|
|
6420
|
+
message: `${(0, import_core19.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
|
|
6277
6421
|
});
|
|
6278
6422
|
await loadModels(deps);
|
|
6279
6423
|
return;
|
|
@@ -6285,7 +6429,7 @@ async function perform(effect, deps) {
|
|
|
6285
6429
|
dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
|
|
6286
6430
|
return;
|
|
6287
6431
|
case "setApiKey": {
|
|
6288
|
-
const meta =
|
|
6432
|
+
const meta = import_core19.ALL_PROVIDERS[effect.provider];
|
|
6289
6433
|
if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
|
|
6290
6434
|
await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
|
|
6291
6435
|
deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
|
|
@@ -6352,13 +6496,23 @@ async function perform(effect, deps) {
|
|
|
6352
6496
|
return;
|
|
6353
6497
|
}
|
|
6354
6498
|
case "loadRewindTargets":
|
|
6355
|
-
dispatch({
|
|
6499
|
+
dispatch({
|
|
6500
|
+
type: "rewindTargetsLoaded",
|
|
6501
|
+
targets: await api.rewindTargets(effect.sessionId),
|
|
6502
|
+
sessionId: effect.sessionId,
|
|
6503
|
+
...effect.pick !== void 0 && { pick: effect.pick }
|
|
6504
|
+
});
|
|
6356
6505
|
return;
|
|
6506
|
+
// A rewind is a fork from an earlier point, so the TUI follows it the way
|
|
6507
|
+
// it follows `/fork`; the original keeps its whole conversation. The
|
|
6508
|
+
// rewound message comes back into the input, to edit and send again.
|
|
6357
6509
|
case "rewindConversation": {
|
|
6358
|
-
const
|
|
6359
|
-
dispatch({ type: "
|
|
6360
|
-
dispatch({ type: "
|
|
6361
|
-
dispatch({ type: "
|
|
6510
|
+
const fork = await api.rewindConversation(effect.sessionId, effect.index);
|
|
6511
|
+
dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
|
|
6512
|
+
dispatch({ type: "chatRestored", history: fork.plan.conversationHistory ?? [], sessionId: fork.sessionId });
|
|
6513
|
+
dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
|
|
6514
|
+
dispatch({ type: "inputPrefilled", text: fork.rewoundMessage, sessionId: fork.sessionId });
|
|
6515
|
+
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
6516
|
return;
|
|
6363
6517
|
}
|
|
6364
6518
|
// The redrawn transcript opens with the summary entry, which is what the
|
|
@@ -6373,15 +6527,17 @@ async function perform(effect, deps) {
|
|
|
6373
6527
|
dispatch({ type: "handoffDiff", diff: await api.reviewRunDiff(effect.sessionId), sessionId: effect.sessionId });
|
|
6374
6528
|
return;
|
|
6375
6529
|
// 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.
|
|
6530
|
+
// tree is exactly as it was, and the words say what to do next. A full
|
|
6531
|
+
// merge leaves nothing to hand over — the daemon has cleared the run up.
|
|
6377
6532
|
case "isolationMerge": {
|
|
6378
|
-
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true);
|
|
6533
|
+
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
|
|
6534
|
+
if (ok) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
6379
6535
|
dispatch({ type: ok ? "notice" : "failed", message });
|
|
6380
6536
|
return;
|
|
6381
6537
|
}
|
|
6382
6538
|
case "isolationDiscard":
|
|
6383
6539
|
await api.discardRun(effect.sessionId);
|
|
6384
|
-
dispatch({ type: "
|
|
6540
|
+
dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
6385
6541
|
dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
|
|
6386
6542
|
return;
|
|
6387
6543
|
case "isolationCleanup":
|
|
@@ -6418,7 +6574,7 @@ async function perform(effect, deps) {
|
|
|
6418
6574
|
return;
|
|
6419
6575
|
case "refresh":
|
|
6420
6576
|
await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
|
|
6421
|
-
dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
6577
|
+
if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
6422
6578
|
return;
|
|
6423
6579
|
case "exit":
|
|
6424
6580
|
deps.exit();
|
|
@@ -6434,7 +6590,7 @@ function osc52(text) {
|
|
|
6434
6590
|
function copySelection(deps, text) {
|
|
6435
6591
|
if (!text) return;
|
|
6436
6592
|
const lines = text.split("\n").length;
|
|
6437
|
-
const command = (0,
|
|
6593
|
+
const command = (0, import_core19.clipboardCopyCommand)(deps.hasBin);
|
|
6438
6594
|
if (command) {
|
|
6439
6595
|
try {
|
|
6440
6596
|
(deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
|
|
@@ -6576,7 +6732,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
6576
6732
|
// The extension shows these in its checkpoint panel; here they are
|
|
6577
6733
|
// transcript lines, which is the TUI's equivalent surface.
|
|
6578
6734
|
case "checkpoint":
|
|
6579
|
-
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0,
|
|
6735
|
+
dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core19.truncateCheckpointSummary)(event.summary)}` });
|
|
6580
6736
|
return;
|
|
6581
6737
|
case "approval_decided":
|
|
6582
6738
|
dispatch({ type: "notice", message: describeApprovalDecision2(event) });
|
|
@@ -6632,8 +6788,10 @@ function onExecutionEvent(dispatch, event, sessionId) {
|
|
|
6632
6788
|
case "approval_request":
|
|
6633
6789
|
case "approval_settled":
|
|
6634
6790
|
return;
|
|
6635
|
-
// Its asker already has the
|
|
6791
|
+
// Its asker already has the words, from the merge request itself; a
|
|
6792
|
+
// viewer that did not ask still has to drop a run that is gone.
|
|
6636
6793
|
case "isolation_merge":
|
|
6794
|
+
if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
|
|
6637
6795
|
return;
|
|
6638
6796
|
default: {
|
|
6639
6797
|
const unhandled = event;
|
|
@@ -6958,7 +7116,7 @@ async function handleTui(subArgs) {
|
|
|
6958
7116
|
}
|
|
6959
7117
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
6960
7118
|
try {
|
|
6961
|
-
(0,
|
|
7119
|
+
(0, import_core20.assertWorkspaceExists)(workspace);
|
|
6962
7120
|
} catch {
|
|
6963
7121
|
console.error(
|
|
6964
7122
|
`Workspace "${workspace}" does not exist.
|
|
@@ -6967,7 +7125,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6967
7125
|
);
|
|
6968
7126
|
process.exit(1);
|
|
6969
7127
|
}
|
|
6970
|
-
registerSkillCommands((0,
|
|
7128
|
+
registerSkillCommands((0, import_core20.createSkillsService)(workspace).listSkills());
|
|
6971
7129
|
const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
|
|
6972
7130
|
const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
|
|
6973
7131
|
const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
|
|
@@ -6986,7 +7144,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
6986
7144
|
conversationQueue,
|
|
6987
7145
|
port,
|
|
6988
7146
|
dispatch: (action) => app.dispatch(action),
|
|
6989
|
-
newSessionId: () => (0,
|
|
7147
|
+
newSessionId: () => (0, import_core20.mintSessionId)(),
|
|
6990
7148
|
setEnvVar: (key, value) => {
|
|
6991
7149
|
writeEnvVar(findEnvFile(), key, value);
|
|
6992
7150
|
process.env[key] = value;
|
|
@@ -7096,7 +7254,7 @@ function cliVersion() {
|
|
|
7096
7254
|
|
|
7097
7255
|
// src/main.ts
|
|
7098
7256
|
async function main() {
|
|
7099
|
-
(0,
|
|
7257
|
+
(0, import_core21.migrateOldConfigDir)();
|
|
7100
7258
|
loadEnvFile();
|
|
7101
7259
|
const argv = process.argv.slice(2);
|
|
7102
7260
|
const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ordewell/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alessandro Costanzo Ciano",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"typecheck": "tsc --noEmit"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@ordewell/core": "^0.5.
|
|
52
|
-
"@ordewell/web": "^0.5.
|
|
51
|
+
"@ordewell/core": "^0.5.1",
|
|
52
|
+
"@ordewell/web": "^0.5.1",
|
|
53
53
|
"string-width": "^7.2.0",
|
|
54
54
|
"ws": "^8.18.0"
|
|
55
55
|
},
|