@ordewell/cli 0.10.4 → 0.10.6
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 +370 -299
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -145,6 +145,7 @@ Planning:
|
|
|
145
145
|
ordewell fork Continue in a copy of the conversation and its tasks
|
|
146
146
|
ordewell compact Condense the conversation into a summary (tasks are kept)
|
|
147
147
|
ordewell rewind [<n>] Fork the conversation from before message <n> (no <n>: list them)
|
|
148
|
+
ordewell resend cancel Keep a planner turn waiting on its usage limit from going at the reset
|
|
148
149
|
ordewell handoff [action] Review, merge, discard or clean up an isolated run
|
|
149
150
|
|
|
150
151
|
Tasks (<id> is an order number or a task ID):
|
|
@@ -153,7 +154,7 @@ Tasks (<id> is an order number or a task ID):
|
|
|
153
154
|
ordewell complete <id> Mark a task complete (alias: mark-complete)
|
|
154
155
|
ordewell uncomplete <id> Mark a completed task not done
|
|
155
156
|
ordewell skip <id> Skip a task: nothing it did lands, dependents go ahead
|
|
156
|
-
ordewell force-start <id>
|
|
157
|
+
ordewell force-start <id> Approve a task and start it now, or once its dependencies finish (asks past a merge gate)
|
|
157
158
|
ordewell run-task <id> Run only one task
|
|
158
159
|
ordewell retry <id> Re-run a failed task
|
|
159
160
|
ordewell continue <id> <msg> Continue a finished task in its saved runner session
|
|
@@ -548,8 +549,7 @@ var ApiClient = class _ApiClient {
|
|
|
548
549
|
...body ? { "Content-Type": "application/json" } : {},
|
|
549
550
|
...token ? { Authorization: (0, import_core4.bearerHeaderValue)(token) } : {}
|
|
550
551
|
},
|
|
551
|
-
// Planner research on a real repository regularly exceeds two minutes
|
|
552
|
-
// The benchmark harness uses the same 15-minute default explicitly;
|
|
552
|
+
// Planner research on a real repository regularly exceeds two minutes;
|
|
553
553
|
// keep the env override for callers that need a tighter/looser bound.
|
|
554
554
|
timeout: Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_HTTP_TIMEOUT_MS
|
|
555
555
|
};
|
|
@@ -631,70 +631,74 @@ var ApiClient = class _ApiClient {
|
|
|
631
631
|
return this.call("POST", `/api/plans/${sessionId}/review/approve`, "Approve failed");
|
|
632
632
|
}
|
|
633
633
|
markTaskComplete(sessionId, taskId) {
|
|
634
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/complete`, "Mark complete failed");
|
|
634
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/complete`, "Mark complete failed");
|
|
635
635
|
}
|
|
636
636
|
/** Skip: end the task without landing its work, so what depends on it goes ahead once the work it waited on is done; never a mark complete. */
|
|
637
637
|
skipTask(sessionId, taskId, reason) {
|
|
638
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/skip`, "Skip failed", reason ? { reason } : {});
|
|
638
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/skip`, "Skip failed", reason ? { reason } : {});
|
|
639
639
|
}
|
|
640
640
|
markTaskIncomplete(sessionId, taskId) {
|
|
641
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`, "Mark not done failed");
|
|
641
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/uncomplete`, "Mark not done failed");
|
|
642
642
|
}
|
|
643
|
-
/** Keep a task paused on a usage limit from resuming on its own;
|
|
643
|
+
/** Keep a task paused on a usage limit from resuming on its own; a task with none scheduled is refused with the reason. */
|
|
644
644
|
cancelResume(sessionId, taskId) {
|
|
645
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/cancel`, "Cancel resume failed");
|
|
645
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resume/cancel`, "Cancel resume failed");
|
|
646
|
+
}
|
|
647
|
+
/** Keep what waits on the planner's usage limit from being sent at the reset; `cancelled` is false when nothing waited. */
|
|
648
|
+
cancelPlannerResend(sessionId) {
|
|
649
|
+
return this.call("POST", `/api/plans/${sessionId}/converse/resend/cancel`, "Cancel resend failed");
|
|
646
650
|
}
|
|
647
651
|
/** Resume a task paused on a usage limit now, in its saved session, once the user says the limit has reset. */
|
|
648
652
|
resumeNow(sessionId, taskId) {
|
|
649
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/now`, "Resume failed");
|
|
653
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resume/now`, "Resume failed");
|
|
650
654
|
}
|
|
651
655
|
/** run | force-start | retry | cancel — real orchestrator work, not a status patch. */
|
|
652
656
|
taskControl(sessionId, taskId, action) {
|
|
653
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/${action}`, `${action} failed`);
|
|
657
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/${action}`, `${action} failed`);
|
|
654
658
|
}
|
|
655
659
|
/** A user message to a structured task: delivered now if it waits for input, else queued behind its turn. */
|
|
656
660
|
sendTaskMessage(sessionId, taskId, text) {
|
|
657
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, "Send message failed", { text });
|
|
661
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages`, "Send message failed", { text });
|
|
658
662
|
}
|
|
659
663
|
/** Force send: interrupt the task's running turn and deliver `text` next, ahead of anything queued (ADR-0023, F1). */
|
|
660
664
|
forceSendTaskMessage(sessionId, taskId, text) {
|
|
661
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, "Send now failed", { text });
|
|
665
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/now`, "Send now failed", { text });
|
|
662
666
|
}
|
|
663
667
|
/** Run the task runner's own command `/name args` as a turn of its own (#66). */
|
|
664
668
|
invokeTaskCommand(sessionId, taskId, name, args) {
|
|
665
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/commands`, "Command failed", { name, args });
|
|
669
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/commands`, "Command failed", { name, args });
|
|
666
670
|
}
|
|
667
|
-
/** Force send a message still queued;
|
|
671
|
+
/** Force send a message still queued; one the runner already has is refused with the reason. */
|
|
668
672
|
forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
|
|
669
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
|
|
673
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
|
|
670
674
|
}
|
|
671
675
|
/** Continue a finished structured task in its saved session, with `text` as its next turn. */
|
|
672
676
|
continueTask(sessionId, taskId, text) {
|
|
673
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, "Continue failed", { text });
|
|
677
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/continue`, "Continue failed", { text });
|
|
674
678
|
}
|
|
675
|
-
/**
|
|
679
|
+
/** Take back a queued message; one already delivered is refused with the reason. */
|
|
676
680
|
removeQueuedTaskMessage(sessionId, taskId, messageId) {
|
|
677
|
-
return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
|
|
681
|
+
return this.call("DELETE", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
|
|
678
682
|
}
|
|
679
683
|
interruptTask(sessionId, taskId) {
|
|
680
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`, "Interrupt failed");
|
|
684
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/interrupt`, "Interrupt failed");
|
|
681
685
|
}
|
|
682
686
|
/** Let a task go on from the checkpoint it waits at; refused, with the reason, when none waits. */
|
|
683
687
|
approveTaskCheckpoint(sessionId, taskId) {
|
|
684
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`, "Approve checkpoint failed");
|
|
688
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/checkpoint/approve`, "Approve checkpoint failed");
|
|
685
689
|
}
|
|
686
690
|
/** Turn a task back at its checkpoint; `reason` is what the agent is told. */
|
|
687
691
|
rejectTaskCheckpoint(sessionId, taskId, reason) {
|
|
688
|
-
return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
|
|
692
|
+
return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
|
|
689
693
|
}
|
|
690
694
|
addTask(sessionId, task) {
|
|
691
695
|
return this.call("POST", `/api/plans/${sessionId}/tasks`, "Add task failed", task);
|
|
692
696
|
}
|
|
693
697
|
updateTask(sessionId, taskId, changes) {
|
|
694
|
-
return this.call("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, "Update task failed", changes);
|
|
698
|
+
return this.call("PUT", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}`, "Update task failed", changes);
|
|
695
699
|
}
|
|
696
700
|
removeTask(sessionId, taskId) {
|
|
697
|
-
return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`, "Remove task failed");
|
|
701
|
+
return this.call("DELETE", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}`, "Remove task failed");
|
|
698
702
|
}
|
|
699
703
|
/**
|
|
700
704
|
* Make a saved session live on the server. `getSession` only reads the file;
|
|
@@ -704,7 +708,7 @@ var ApiClient = class _ApiClient {
|
|
|
704
708
|
async adoptSession(sessionId, workspace) {
|
|
705
709
|
const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
|
|
706
710
|
const res = await this.call("POST", `/api/sessions/${sessionId}/load${qs}`, "Failed to load session");
|
|
707
|
-
return { plan: res.plan, goal: res.goal ?? "" };
|
|
711
|
+
return { plan: res.plan, goal: res.goal ?? "", ...res.run ? { run: res.run } : {} };
|
|
708
712
|
}
|
|
709
713
|
deleteSession(sessionId, workspace) {
|
|
710
714
|
const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
|
|
@@ -735,7 +739,7 @@ var ApiClient = class _ApiClient {
|
|
|
735
739
|
}
|
|
736
740
|
/** The ancestors a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
|
|
737
741
|
async getMergeGate(sessionId, taskId) {
|
|
738
|
-
const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`, "Could not read the merge gate");
|
|
742
|
+
const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/merge-gate`, "Could not read the merge gate");
|
|
739
743
|
return res.mergeGate ?? [];
|
|
740
744
|
}
|
|
741
745
|
async reviewRunDiff(sessionId) {
|
|
@@ -765,7 +769,7 @@ var ApiClient = class _ApiClient {
|
|
|
765
769
|
await this.call("POST", `/api/plans/${sessionId}/isolation/${segment}`, failure);
|
|
766
770
|
}
|
|
767
771
|
async resolveConflictAsTask(sessionId, taskId) {
|
|
768
|
-
const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`, "Could not add a resolver task");
|
|
772
|
+
const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resolve-conflict`, "Could not add a resolver task");
|
|
769
773
|
return res.plan;
|
|
770
774
|
}
|
|
771
775
|
async closeSession(sessionId) {
|
|
@@ -794,13 +798,13 @@ var ApiClient = class _ApiClient {
|
|
|
794
798
|
async getTaskLogAttempts(sessionId, taskId, workspace) {
|
|
795
799
|
const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
|
|
796
800
|
const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`, "Could not read the task log");
|
|
797
|
-
return res
|
|
801
|
+
return res;
|
|
798
802
|
}
|
|
799
803
|
/** One attempt's saved log, for `replayTaskLog`. */
|
|
800
804
|
async getTaskLog(sessionId, taskId, attempt, workspace) {
|
|
801
805
|
const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
|
|
802
806
|
const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`, "Could not read the task log");
|
|
803
|
-
return res
|
|
807
|
+
return res;
|
|
804
808
|
}
|
|
805
809
|
/**
|
|
806
810
|
* The one place a session socket is built, so the token travels on both
|
|
@@ -1041,12 +1045,7 @@ function allTasksOf(plan) {
|
|
|
1041
1045
|
return (0, import_core5.rejoinPlanTree)(tasks, executionLog);
|
|
1042
1046
|
}
|
|
1043
1047
|
function resolveTaskId(plan, identifier) {
|
|
1044
|
-
|
|
1045
|
-
const byRef = (0, import_core5.resolveTaskRef)(allTasks, identifier);
|
|
1046
|
-
if (byRef) return byRef.id;
|
|
1047
|
-
const prefixMatches = allTasks.filter((t) => typeof t.id === "string" && t.id.startsWith(identifier));
|
|
1048
|
-
if (prefixMatches.length === 1) return prefixMatches[0].id;
|
|
1049
|
-
return void 0;
|
|
1048
|
+
return (0, import_core5.resolveUserTaskRef)(allTasksOf(plan), identifier)?.id;
|
|
1050
1049
|
}
|
|
1051
1050
|
|
|
1052
1051
|
// src/utils/session.ts
|
|
@@ -1610,10 +1609,21 @@ var STATE_OF = {
|
|
|
1610
1609
|
function reposOf(run) {
|
|
1611
1610
|
if (Array.isArray(run.repos)) {
|
|
1612
1611
|
const repos = run.repos.filter((r) => r && typeof r.path === "string" && typeof r.integrationBranch === "string" && typeof r.baseRef === "string");
|
|
1613
|
-
return repos.length > 0 ? repos.map((r) => ({
|
|
1612
|
+
return repos.length > 0 ? repos.map((r) => ({
|
|
1613
|
+
path: String(r.path),
|
|
1614
|
+
integrationBranch: String(r.integrationBranch),
|
|
1615
|
+
baseRef: String(r.baseRef),
|
|
1616
|
+
...typeof r.baseBranch === "string" && r.baseBranch ? { baseBranch: r.baseBranch } : {},
|
|
1617
|
+
...r.startedDetached === true ? { startedDetached: true } : {}
|
|
1618
|
+
})) : null;
|
|
1614
1619
|
}
|
|
1615
1620
|
if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
|
|
1616
|
-
return [{
|
|
1621
|
+
return [{
|
|
1622
|
+
path: ".",
|
|
1623
|
+
integrationBranch: run.integrationBranch,
|
|
1624
|
+
baseRef: run.baseRef,
|
|
1625
|
+
...typeof run.baseBranch === "string" && run.baseBranch ? { baseBranch: run.baseBranch } : {}
|
|
1626
|
+
}];
|
|
1617
1627
|
}
|
|
1618
1628
|
return null;
|
|
1619
1629
|
}
|
|
@@ -1664,15 +1674,11 @@ function handoffBranch(handoff) {
|
|
|
1664
1674
|
return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
|
|
1665
1675
|
}
|
|
1666
1676
|
function handoffBase(handoff, length) {
|
|
1667
|
-
const group = isRepoGroup(handoff);
|
|
1668
|
-
return handoff.repos.map((r) =>
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
}
|
|
1673
|
-
function reposWithWork(handoff) {
|
|
1674
|
-
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
1675
|
-
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
1677
|
+
const group = (0, import_core10.isRepoGroup)(handoff);
|
|
1678
|
+
return handoff.repos.map((r) => {
|
|
1679
|
+
const ref = r.baseRef.slice(0, length);
|
|
1680
|
+
return `${group ? `${r.path}: ` : ""}${r.baseBranch ? `${r.baseBranch} @ ${ref}` : `${r.startedDetached ? "detached at" : "base"} ${ref}`}`;
|
|
1681
|
+
}).join(", ");
|
|
1676
1682
|
}
|
|
1677
1683
|
function repairedLanded(handoff) {
|
|
1678
1684
|
return handoff.landed.filter((t) => t.repairedFiles?.length);
|
|
@@ -1750,7 +1756,7 @@ async function followExecution(api, sessionId, start, onBlocked) {
|
|
|
1750
1756
|
for (const t of entries) {
|
|
1751
1757
|
const icon = iconFor(t.status);
|
|
1752
1758
|
const verdict = t.verdict ? `: ${t.verdict.outcome.toUpperCase()}: ${t.verdict.reason.slice(0, 60)}` : "";
|
|
1753
|
-
const waits = t.waitsForApproval ? `: ${inLine(import_core12.WAITS_FOR_APPROVAL_LABEL)}: \`ordewell
|
|
1759
|
+
const waits = t.waitsForApproval ? `: ${inLine(import_core12.WAITS_FOR_APPROVAL_LABEL)}: \`ordewell force-start <id>\` approves it, \`ordewell run\` approves all` : "";
|
|
1754
1760
|
lines.push(` ${icon} #${t.id.slice(-4)} ${t.status}${verdict}${waits}`);
|
|
1755
1761
|
}
|
|
1756
1762
|
const output = lines.join("\n");
|
|
@@ -1812,10 +1818,10 @@ Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summ
|
|
|
1812
1818
|
}
|
|
1813
1819
|
if (event.type === "isolation_handoff") {
|
|
1814
1820
|
const n = event.landed.length;
|
|
1815
|
-
const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
|
|
1821
|
+
const where = (0, import_core10.isRepoGroup)(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
|
|
1816
1822
|
console.log(`
|
|
1817
1823
|
Run finished on ${handoffBranch(event)}${where}: ${n} task${n === 1 ? "" : "s"} landed.`);
|
|
1818
|
-
if (isRepoGroup(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
|
|
1824
|
+
if ((0, import_core10.isRepoGroup)(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
|
|
1819
1825
|
if (event.deferredTo === "supervisor") console.log(" The supervisor may merge it.");
|
|
1820
1826
|
console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
|
|
1821
1827
|
}
|
|
@@ -1852,7 +1858,8 @@ async function handleRun(subArgs, injectedApi) {
|
|
|
1852
1858
|
const onBlocked = hasFlag(subArgs, "--stash") ? "stash" : hasFlag(subArgs, "--without-isolation") ? "shared" : void 0;
|
|
1853
1859
|
await followExecution(api, sessionId, async () => {
|
|
1854
1860
|
try {
|
|
1855
|
-
await api.executePlan(sessionId);
|
|
1861
|
+
const started = await api.executePlan(sessionId);
|
|
1862
|
+
if (started.message) console.error(started.message);
|
|
1856
1863
|
} catch (err) {
|
|
1857
1864
|
console.error(`Failed to start execution: ${err.message}`);
|
|
1858
1865
|
process.exit(1);
|
|
@@ -2656,27 +2663,28 @@ function formatCost(currency, amount) {
|
|
|
2656
2663
|
function tokenLine(blocks2, cols) {
|
|
2657
2664
|
const usage = blocks2[blocks2.length - 1];
|
|
2658
2665
|
if (usage?.type !== "usage") return null;
|
|
2659
|
-
const parts = usageMeasures(usage.totals
|
|
2666
|
+
const parts = usageMeasures(usage.totals);
|
|
2660
2667
|
if (usage.model && !usage.contextFill) parts.push("ctx unknown");
|
|
2661
2668
|
if (usage.contextFill) parts.splice(2, 0, `${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}% ctx`);
|
|
2662
2669
|
return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
|
|
2663
2670
|
}
|
|
2664
|
-
function usageMeasures(totals
|
|
2671
|
+
function usageMeasures(totals) {
|
|
2665
2672
|
return [
|
|
2666
|
-
...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] :
|
|
2667
|
-
...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] :
|
|
2673
|
+
...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : [],
|
|
2674
|
+
...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : [],
|
|
2668
2675
|
...totals.cachedInputTokens !== void 0 ? [`${compactCount(totals.cachedInputTokens)} cached`] : [],
|
|
2669
|
-
...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount))
|
|
2670
|
-
...unknown && !Object.keys(totals.reportedCost ?? {}).length ? ["cost unknown"] : []
|
|
2676
|
+
...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount))
|
|
2671
2677
|
];
|
|
2672
2678
|
}
|
|
2673
2679
|
function taskUsageText(usage) {
|
|
2674
2680
|
return [
|
|
2675
|
-
...usageMeasures(usage.totals
|
|
2676
|
-
...usage.attempt !== void 0 ? [usage.contextFill ?
|
|
2677
|
-
usage.wallTimeMs !== void 0 ? `${Math.floor(usage.wallTimeMs / 1e3)}s` : "time unknown"
|
|
2681
|
+
...usageMeasures(usage.totals),
|
|
2682
|
+
...usage.attempt !== void 0 ? [usage.contextFill ? `ctx ${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}%` : "ctx unknown"] : []
|
|
2678
2683
|
].join(" \xB7 ");
|
|
2679
2684
|
}
|
|
2685
|
+
function assignmentFlagText(row) {
|
|
2686
|
+
return `${row.assignmentFlag} \u2014 ${row.assignmentConsequence}`;
|
|
2687
|
+
}
|
|
2680
2688
|
|
|
2681
2689
|
// src/commands/status.ts
|
|
2682
2690
|
var import_fs4 = require("fs");
|
|
@@ -2694,7 +2702,7 @@ async function handleStatus(subArgs, injectedApi) {
|
|
|
2694
2702
|
emitJson(session, outputPath);
|
|
2695
2703
|
return;
|
|
2696
2704
|
}
|
|
2697
|
-
printOneSession(sessionId, session.meta, session.plan);
|
|
2705
|
+
printOneSession(sessionId, session.meta, session.plan, await assignmentScope(api));
|
|
2698
2706
|
} else {
|
|
2699
2707
|
const sessions = await api.getSessions(workspace);
|
|
2700
2708
|
if (asJson) {
|
|
@@ -2733,7 +2741,20 @@ function emitJson(payload, outputPath) {
|
|
|
2733
2741
|
process.exit(1);
|
|
2734
2742
|
}
|
|
2735
2743
|
}
|
|
2736
|
-
function
|
|
2744
|
+
async function assignmentScope(api) {
|
|
2745
|
+
try {
|
|
2746
|
+
const [runners2, settings, models] = await Promise.all([api.getRunners(), api.getSettings(), api.getModels()]);
|
|
2747
|
+
if (!Array.isArray(runners2.runners) || runners2.runners.length === 0) return void 0;
|
|
2748
|
+
return {
|
|
2749
|
+
enabledRunners: runners2.runners.filter((r) => r.enabled).map((r) => r.id),
|
|
2750
|
+
allowlist: settings.modelAllowlist ?? {},
|
|
2751
|
+
models: models.modelsByRunner ?? {}
|
|
2752
|
+
};
|
|
2753
|
+
} catch {
|
|
2754
|
+
return void 0;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
function printOneSession(sessionId, meta, plan, runnerScope) {
|
|
2737
2758
|
const allTasks = allTasksOf(plan);
|
|
2738
2759
|
const everyTask = (0, import_core15.flattenTasks)(allTasks);
|
|
2739
2760
|
const blocked = (0, import_core15.blockingFailures)(everyTask.map((t) => ({ ...t, subtasks: [], dependencies: t.dependencies ?? [] })));
|
|
@@ -2742,20 +2763,23 @@ function printOneSession(sessionId, meta, plan) {
|
|
|
2742
2763
|
console.log(`Goal: ${meta.goal}`);
|
|
2743
2764
|
console.log(`Runner: ${(meta.runners || []).join(", ")} | Status: ${meta.status}`);
|
|
2744
2765
|
console.log(`Phase: ${plan?.phase || "unknown"} | Created: ${meta.createdAt}`);
|
|
2766
|
+
const isolation = isolationOfPlan(plan);
|
|
2767
|
+
if (isolation) console.log(`Base: ${handoffBase(isolation.handoff, 8)}`);
|
|
2745
2768
|
console.log(`Tasks: ${everyTask.length} total | ${count("completed")} done | ${count("failed")} failed | ${count("in_progress")} running | ${count("blocked")} blocked | ${count("skipped")} skipped
|
|
2746
2769
|
`);
|
|
2747
2770
|
const usage = (0, import_core15.planUsage)(allTasks);
|
|
2748
2771
|
if (usage.attempts) console.log(`Tasks total: ${taskUsageText(usage)}`);
|
|
2749
2772
|
const record = servedApprovalRecord(plan);
|
|
2750
|
-
const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell
|
|
2773
|
+
const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell force-start ${ref}\` approves it, \`ordewell run\` approves all` : null;
|
|
2751
2774
|
for (const t of allTasks) {
|
|
2752
2775
|
const icon = iconFor(blocked.has(t.id) ? "blocked" : t.status);
|
|
2753
2776
|
const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
|
|
2754
2777
|
const shortId = (t.id ?? "").slice(-8);
|
|
2755
2778
|
console.log(` ${(0, import_core15.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
|
|
2756
|
-
const row = (0, import_core15.taskRowView)({ ...t, dependencies: t.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(t, record) }, { tasks: allTasks });
|
|
2779
|
+
const row = (0, import_core15.taskRowView)({ ...t, dependencies: t.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(t, record) }, { tasks: allTasks, runnerScope });
|
|
2757
2780
|
const waiting = approval(row, (0, import_core15.taskOrderLabel)(t));
|
|
2758
2781
|
if (waiting) console.log(` ${waiting}`);
|
|
2782
|
+
if (row.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(row)}`);
|
|
2759
2783
|
if (row.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(row.blockedBy)}`);
|
|
2760
2784
|
if (row.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(row.skipReason)}`);
|
|
2761
2785
|
if (row.usage) console.log(` ${taskUsageText(row.usage)}`);
|
|
@@ -2764,9 +2788,10 @@ function printOneSession(sessionId, meta, plan) {
|
|
|
2764
2788
|
const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
|
|
2765
2789
|
const subShortId = (sub.id ?? "").slice(-8);
|
|
2766
2790
|
console.log(` ${(0, import_core15.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
|
|
2767
|
-
const subRow = (0, import_core15.taskRowView)({ ...sub, dependencies: sub.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(sub, record) }, { parent: t, tasks: allTasks });
|
|
2791
|
+
const subRow = (0, import_core15.taskRowView)({ ...sub, dependencies: sub.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(sub, record) }, { parent: t, tasks: allTasks, runnerScope });
|
|
2768
2792
|
const subWaiting = approval(subRow, (0, import_core15.taskOrderLabel)(sub, t));
|
|
2769
2793
|
if (subWaiting) console.log(` ${subWaiting}`);
|
|
2794
|
+
if (subRow.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(subRow)}`);
|
|
2770
2795
|
if (subRow.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(subRow.blockedBy)}`);
|
|
2771
2796
|
if (subRow.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(subRow.skipReason)}`);
|
|
2772
2797
|
if (subRow.usage) console.log(` ${taskUsageText(subRow.usage)}`);
|
|
@@ -2779,7 +2804,7 @@ function servedApprovalRecord(plan) {
|
|
|
2779
2804
|
}
|
|
2780
2805
|
|
|
2781
2806
|
// src/commands/stop.ts
|
|
2782
|
-
async function handleStop(subArgs) {
|
|
2807
|
+
async function handleStop(subArgs, injectedApi) {
|
|
2783
2808
|
if (subArgs.includes("--server") || subArgs[0] === "server") {
|
|
2784
2809
|
const stopped = await stopDaemon(resolvePort(subArgs));
|
|
2785
2810
|
if (!stopped) process.exit(1);
|
|
@@ -2796,10 +2821,9 @@ async function handleStop(subArgs) {
|
|
|
2796
2821
|
sessionId = last.sessionId;
|
|
2797
2822
|
console.error(`Stopping last session: ${sessionId}`);
|
|
2798
2823
|
}
|
|
2799
|
-
const
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
console.log("Execution stopped.");
|
|
2824
|
+
const api = await connect(subArgs, injectedApi);
|
|
2825
|
+
const { message } = await api.stopExecution(sessionId);
|
|
2826
|
+
console.log(message ?? "Execution stopped.");
|
|
2803
2827
|
}
|
|
2804
2828
|
|
|
2805
2829
|
// src/commands/web.ts
|
|
@@ -3214,7 +3238,7 @@ var PAST_TENSE = {
|
|
|
3214
3238
|
retry: "Task retried.",
|
|
3215
3239
|
cancel: "Task cancelled."
|
|
3216
3240
|
};
|
|
3217
|
-
async function
|
|
3241
|
+
async function sessionAndRef(subArgs, usage, injectedApi) {
|
|
3218
3242
|
const workspace = flag(subArgs, "--workspace") || process.cwd();
|
|
3219
3243
|
let sessionId = flag(subArgs, "--session-id");
|
|
3220
3244
|
if (!sessionId) {
|
|
@@ -3225,12 +3249,20 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
|
|
|
3225
3249
|
}
|
|
3226
3250
|
sessionId = last.sessionId;
|
|
3227
3251
|
}
|
|
3228
|
-
const
|
|
3229
|
-
if (!
|
|
3252
|
+
const ref = positionals(subArgs)[0];
|
|
3253
|
+
if (!ref) {
|
|
3230
3254
|
console.error(usage);
|
|
3231
3255
|
process.exit(1);
|
|
3232
3256
|
}
|
|
3233
3257
|
const api = injectedApi || new ApiClient(await ensureDaemon(resolvePort(subArgs)));
|
|
3258
|
+
return { api, sessionId, ref, workspace };
|
|
3259
|
+
}
|
|
3260
|
+
async function withTaskRef(subArgs, usage, injectedApi, run) {
|
|
3261
|
+
const { api, sessionId, ref } = await sessionAndRef(subArgs, usage, injectedApi);
|
|
3262
|
+
await run(api, sessionId, ref);
|
|
3263
|
+
}
|
|
3264
|
+
async function withResolvedTask(subArgs, usage, injectedApi, run) {
|
|
3265
|
+
const { api, sessionId, ref, workspace } = await sessionAndRef(subArgs, usage, injectedApi);
|
|
3234
3266
|
let plan;
|
|
3235
3267
|
try {
|
|
3236
3268
|
plan = (await api.getSession(sessionId, workspace)).plan;
|
|
@@ -3238,35 +3270,37 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
|
|
|
3238
3270
|
console.error(`Failed to load session: ${err.message}`);
|
|
3239
3271
|
process.exit(1);
|
|
3240
3272
|
}
|
|
3241
|
-
const taskId = resolveTaskId(plan,
|
|
3273
|
+
const taskId = resolveTaskId(plan, ref);
|
|
3242
3274
|
if (!taskId) {
|
|
3243
|
-
console.error(`Task not found: "${
|
|
3275
|
+
console.error(`Task not found: "${ref}"`);
|
|
3244
3276
|
process.exit(1);
|
|
3245
3277
|
}
|
|
3246
3278
|
await run(api, sessionId, taskId, plan);
|
|
3247
3279
|
}
|
|
3248
3280
|
var PASSES_GATE = /* @__PURE__ */ new Set(["run", "force-start"]);
|
|
3249
|
-
async function confirmPastGate(subArgs, api, sessionId,
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
if (
|
|
3281
|
+
async function confirmPastGate(subArgs, api, sessionId, ref, confirm2) {
|
|
3282
|
+
const unmerged = await api.getMergeGate(sessionId, ref).catch(() => []);
|
|
3283
|
+
if (unmerged.length === 0) return false;
|
|
3284
|
+
if (hasFlag(subArgs, "--yes")) return true;
|
|
3253
3285
|
const question = `${(0, import_core21.pastGateConfirmation)(unmerged.map((d) => (0, import_core21.titledTaskRef)(d)))} Start it anyway?`;
|
|
3254
3286
|
if (!await confirm2(question)) fail("Not started: nothing was changed. Pass --yes to start it without a prompt.");
|
|
3287
|
+
return true;
|
|
3255
3288
|
}
|
|
3256
3289
|
function makeHandler(action, command = action) {
|
|
3257
3290
|
const yes = PASSES_GATE.has(action) ? " [--yes]" : "";
|
|
3258
3291
|
return async function handle(subArgs, injectedApi, confirm2 = askYesNo) {
|
|
3259
|
-
await
|
|
3292
|
+
await withTaskRef(
|
|
3260
3293
|
subArgs,
|
|
3261
3294
|
`Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]${yes}`,
|
|
3262
3295
|
injectedApi,
|
|
3263
|
-
async (api, sessionId,
|
|
3264
|
-
|
|
3296
|
+
async (api, sessionId, ref) => {
|
|
3297
|
+
const pastGate = PASSES_GATE.has(action) ? await confirmPastGate(subArgs, api, sessionId, ref, confirm2) : false;
|
|
3298
|
+
const control = pastGate && action === "run" ? "force-start" : action;
|
|
3265
3299
|
try {
|
|
3266
|
-
await api.taskControl(sessionId,
|
|
3267
|
-
console.log(PAST_TENSE[
|
|
3300
|
+
const { message, queued } = await api.taskControl(sessionId, ref, control);
|
|
3301
|
+
console.log(message ?? queued ?? PAST_TENSE[control]);
|
|
3268
3302
|
} catch (err) {
|
|
3269
|
-
console.error(`Failed to ${
|
|
3303
|
+
console.error(`Failed to ${control} task: ${err.message}`);
|
|
3270
3304
|
process.exit(1);
|
|
3271
3305
|
}
|
|
3272
3306
|
}
|
|
@@ -3280,10 +3314,10 @@ async function handleContinue(subArgs, injectedApi) {
|
|
|
3280
3314
|
console.error(usage);
|
|
3281
3315
|
process.exit(1);
|
|
3282
3316
|
}
|
|
3283
|
-
await
|
|
3317
|
+
await withTaskRef(subArgs, usage, injectedApi, async (api, sessionId, ref) => {
|
|
3284
3318
|
try {
|
|
3285
|
-
await api.continueTask(sessionId,
|
|
3286
|
-
console.log("Task continued.");
|
|
3319
|
+
const sent = await api.continueTask(sessionId, ref, message);
|
|
3320
|
+
console.log(sent.message ?? "Task continued.");
|
|
3287
3321
|
} catch (err) {
|
|
3288
3322
|
console.error(`Failed to continue task: ${err.message}`);
|
|
3289
3323
|
process.exit(1);
|
|
@@ -3303,11 +3337,10 @@ async function handleCheckpoint(subArgs, injectedApi) {
|
|
|
3303
3337
|
${usage}`);
|
|
3304
3338
|
process.exit(1);
|
|
3305
3339
|
}
|
|
3306
|
-
await
|
|
3340
|
+
await withTaskRef(subArgs, usage, injectedApi, async (api, sessionId, ref) => {
|
|
3307
3341
|
try {
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
console.log(answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected.");
|
|
3342
|
+
const sent = answer === "approve" ? await api.approveTaskCheckpoint(sessionId, ref) : await api.rejectTaskCheckpoint(sessionId, ref, reason || void 0);
|
|
3343
|
+
console.log(sent.message ?? (answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected."));
|
|
3311
3344
|
} catch (err) {
|
|
3312
3345
|
console.error(`Failed to ${answer} checkpoint: ${err.message}`);
|
|
3313
3346
|
process.exit(1);
|
|
@@ -3321,14 +3354,14 @@ var handleCancel = makeHandler("cancel");
|
|
|
3321
3354
|
|
|
3322
3355
|
// src/commands/mark-complete.ts
|
|
3323
3356
|
async function handleMarkComplete(subArgs, api) {
|
|
3324
|
-
await
|
|
3357
|
+
await withTaskRef(
|
|
3325
3358
|
subArgs,
|
|
3326
3359
|
"Usage: ordewell mark-complete <task-id-or-order> [--session-id <id>]",
|
|
3327
3360
|
api,
|
|
3328
|
-
async (client, sessionId,
|
|
3361
|
+
async (client, sessionId, ref) => {
|
|
3329
3362
|
try {
|
|
3330
|
-
await client.markTaskComplete(sessionId,
|
|
3331
|
-
console.log("Task marked complete.");
|
|
3363
|
+
const done = await client.markTaskComplete(sessionId, ref);
|
|
3364
|
+
console.log(done.message ?? "Task marked complete.");
|
|
3332
3365
|
} catch (err) {
|
|
3333
3366
|
console.error(`Failed to mark task complete: ${err.message}`);
|
|
3334
3367
|
process.exit(1);
|
|
@@ -3337,14 +3370,14 @@ async function handleMarkComplete(subArgs, api) {
|
|
|
3337
3370
|
);
|
|
3338
3371
|
}
|
|
3339
3372
|
async function handleUncomplete(subArgs, api) {
|
|
3340
|
-
await
|
|
3373
|
+
await withTaskRef(
|
|
3341
3374
|
subArgs,
|
|
3342
3375
|
"Usage: ordewell uncomplete <task-id-or-order> [--session-id <id>]",
|
|
3343
3376
|
api,
|
|
3344
|
-
async (client, sessionId,
|
|
3377
|
+
async (client, sessionId, ref) => {
|
|
3345
3378
|
try {
|
|
3346
|
-
await client.markTaskIncomplete(sessionId,
|
|
3347
|
-
console.log("Task marked not done.");
|
|
3379
|
+
const done = await client.markTaskIncomplete(sessionId, ref);
|
|
3380
|
+
console.log(done.message ?? "Task marked not done.");
|
|
3348
3381
|
} catch (err) {
|
|
3349
3382
|
console.error(`Failed to mark task not done: ${err.message}`);
|
|
3350
3383
|
process.exit(1);
|
|
@@ -3353,14 +3386,14 @@ async function handleUncomplete(subArgs, api) {
|
|
|
3353
3386
|
);
|
|
3354
3387
|
}
|
|
3355
3388
|
async function handleSkip(subArgs, api) {
|
|
3356
|
-
await
|
|
3389
|
+
await withTaskRef(
|
|
3357
3390
|
subArgs,
|
|
3358
3391
|
"Usage: ordewell skip <task-id-or-order> [--reason <text>] [--session-id <id>]",
|
|
3359
3392
|
api,
|
|
3360
|
-
async (client, sessionId,
|
|
3393
|
+
async (client, sessionId, ref) => {
|
|
3361
3394
|
try {
|
|
3362
|
-
await client.skipTask(sessionId,
|
|
3363
|
-
console.log("Task skipped: none of its work lands, and the tasks that depend on it go ahead without it once the work it waited on is done.");
|
|
3395
|
+
const done = await client.skipTask(sessionId, ref, flag(subArgs, "--reason"));
|
|
3396
|
+
console.log(done.message ?? "Task skipped: none of its work lands, and the tasks that depend on it go ahead without it once the work it waited on is done.");
|
|
3364
3397
|
} catch (err) {
|
|
3365
3398
|
console.error(`Failed to skip task: ${err.message}`);
|
|
3366
3399
|
process.exit(1);
|
|
@@ -3561,6 +3594,17 @@ async function handleRewind(subArgs, injectedApi) {
|
|
|
3561
3594
|
console.log("\nThe message you rewound to, to resend or edit:\n");
|
|
3562
3595
|
console.log(fork.rewoundMessage);
|
|
3563
3596
|
}
|
|
3597
|
+
async function handleResend(subArgs, injectedApi) {
|
|
3598
|
+
if (positionals(subArgs)[0] !== "cancel") fail("Usage: ordewell resend cancel [--session-id <id>] [--workspace /path]");
|
|
3599
|
+
const { api, sessionId } = await adopted(subArgs, injectedApi);
|
|
3600
|
+
let cancelled;
|
|
3601
|
+
try {
|
|
3602
|
+
({ cancelled } = await api.cancelPlannerResend(sessionId));
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
fail(`Failed to cancel the resend: ${err.message}`);
|
|
3605
|
+
}
|
|
3606
|
+
console.log(cancelled ? "Resend cancelled: what waits goes with your next message." : "Nothing waits on the planner's usage limit to be sent at a reset.");
|
|
3607
|
+
}
|
|
3564
3608
|
|
|
3565
3609
|
// src/tui/transcript.ts
|
|
3566
3610
|
var import_core22 = require("@ordewell/core");
|
|
@@ -3594,7 +3638,7 @@ var HANDOFF_ACTIONS = [
|
|
|
3594
3638
|
{ id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
|
|
3595
3639
|
];
|
|
3596
3640
|
function handoffActions(handoff) {
|
|
3597
|
-
if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
|
|
3641
|
+
if (!(0, import_core10.isRepoGroup)(handoff)) return HANDOFF_ACTIONS;
|
|
3598
3642
|
return [
|
|
3599
3643
|
{ id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
|
|
3600
3644
|
{ id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there (asks first)" },
|
|
@@ -3619,7 +3663,7 @@ function handoffCommand(state, arg) {
|
|
|
3619
3663
|
}
|
|
3620
3664
|
function handoffArrived(state, handoff) {
|
|
3621
3665
|
const landed = handoff.landed.length;
|
|
3622
|
-
const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
|
|
3666
|
+
const where = (0, import_core10.isRepoGroup)(handoff) ? ` in ${reposOf2(handoff)}` : "";
|
|
3623
3667
|
const branch = handoffBranch(handoff);
|
|
3624
3668
|
const tasks = `${landed} task${landed === 1 ? "" : "s"} landed`;
|
|
3625
3669
|
const received = { ...state, handoff };
|
|
@@ -3634,9 +3678,6 @@ function handoffArrived(state, handoff) {
|
|
|
3634
3678
|
);
|
|
3635
3679
|
return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
|
|
3636
3680
|
}
|
|
3637
|
-
function withDeferral(fresh, known) {
|
|
3638
|
-
return known?.deferredTo && handoffBranch(known) === handoffBranch(fresh) ? { ...fresh, deferredTo: known.deferredTo } : fresh;
|
|
3639
|
-
}
|
|
3640
3681
|
function runHandoffAction(state, id) {
|
|
3641
3682
|
const { sessionId } = state;
|
|
3642
3683
|
const handoff = currentHandoff(state);
|
|
@@ -3654,8 +3695,8 @@ function runHandoffAction(state, id) {
|
|
|
3654
3695
|
...state,
|
|
3655
3696
|
overlay: {
|
|
3656
3697
|
kind: "confirm",
|
|
3657
|
-
title: isRepoGroup(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
|
|
3658
|
-
message: `Merge ${handoffBranch(handoff)} as it stands now into
|
|
3698
|
+
title: (0, import_core10.isRepoGroup)(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
|
|
3699
|
+
message: `Merge ${handoffBranch(handoff)} as it stands now into ${(0, import_core10.mergeTarget)(handoff)}.${(0, import_core10.checkoutRule)(handoff)} The run goes on: tasks waiting for Merge all start once it has merged, and later tasks land on the same branch for the next Merge all. A merge that conflicts is aborted, and your ${(0, import_core10.isRepoGroup)(handoff) ? "trees stay" : "tree stays"} as ${(0, import_core10.isRepoGroup)(handoff) ? "they were" : "it was"}.${repairedNotice(handoff)}`,
|
|
3659
3700
|
action: { kind: "merge-run" }
|
|
3660
3701
|
}
|
|
3661
3702
|
},
|
|
@@ -3665,15 +3706,15 @@ function runHandoffAction(state, id) {
|
|
|
3665
3706
|
return {
|
|
3666
3707
|
state: {
|
|
3667
3708
|
...state,
|
|
3668
|
-
overlay: isRepoGroup(handoff) ? {
|
|
3709
|
+
overlay: (0, import_core10.isRepoGroup)(handoff) ? {
|
|
3669
3710
|
kind: "confirm",
|
|
3670
3711
|
title: "Merge all into your branches?",
|
|
3671
|
-
message: `Merge ${handoffBranch(handoff)} into
|
|
3712
|
+
message: `Merge ${handoffBranch(handoff)} into ${(0, import_core10.mergeTarget)(handoff)}.${(0, import_core10.checkoutRule)(handoff)} 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 unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
|
|
3672
3713
|
action: { kind: "merge-run" }
|
|
3673
3714
|
} : {
|
|
3674
3715
|
kind: "confirm",
|
|
3675
3716
|
title: "Merge into your branch?",
|
|
3676
|
-
message: `Merge ${handoffBranch(handoff)} into
|
|
3717
|
+
message: `Merge ${handoffBranch(handoff)} into ${(0, import_core10.mergeTarget)(handoff)}.${(0, import_core10.checkoutRule)(handoff)} 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 unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
|
|
3677
3718
|
action: { kind: "merge-run" }
|
|
3678
3719
|
}
|
|
3679
3720
|
},
|
|
@@ -3686,7 +3727,7 @@ function runHandoffAction(state, id) {
|
|
|
3686
3727
|
overlay: {
|
|
3687
3728
|
kind: "confirm",
|
|
3688
3729
|
title: "Discard this run?",
|
|
3689
|
-
message: `Removes the run's worktrees and task branches and deletes ${handoffBranch(handoff)}${isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : ""}. Tasks keep their status: mark one not done if you did not keep its work.`,
|
|
3730
|
+
message: `Removes the run's worktrees and task branches and deletes ${handoffBranch(handoff)}${(0, import_core10.isRepoGroup)(handoff) ? ` in ${reposOf2(handoff)}` : ""}. Tasks keep their status: mark one not done if you did not keep its work.`,
|
|
3690
3731
|
action: { kind: "discard-run" }
|
|
3691
3732
|
}
|
|
3692
3733
|
},
|
|
@@ -3704,9 +3745,8 @@ function confirmedHandoff(state, kind) {
|
|
|
3704
3745
|
type: "isolationMerge",
|
|
3705
3746
|
sessionId,
|
|
3706
3747
|
branch: handoffBranch(handoff),
|
|
3707
|
-
...isRepoGroup(handoff) ? { group: true } : {},
|
|
3708
|
-
...repaired.length > 0 ? { repaired } : {}
|
|
3709
|
-
...midRun(state) ? { midRun: true } : {}
|
|
3748
|
+
...(0, import_core10.isRepoGroup)(handoff) ? { group: true } : {},
|
|
3749
|
+
...repaired.length > 0 ? { repaired } : {}
|
|
3710
3750
|
} : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
|
|
3711
3751
|
return { state: closed, effects: [effect] };
|
|
3712
3752
|
}
|
|
@@ -3789,7 +3829,7 @@ async function handleHandoff(subArgs, injectedApi, confirm2 = askYesNo) {
|
|
|
3789
3829
|
const handoff = isolationOfPlan(plan)?.handoff;
|
|
3790
3830
|
if (!handoff) fail("This session has no isolated run to hand off.");
|
|
3791
3831
|
const branch = handoffBranch(handoff);
|
|
3792
|
-
const group = isRepoGroup(handoff);
|
|
3832
|
+
const group = (0, import_core10.isRepoGroup)(handoff);
|
|
3793
3833
|
if (group) for (const line of repoResultLines(handoff)) console.error(line);
|
|
3794
3834
|
const asked = hasFlag(subArgs, "--yes");
|
|
3795
3835
|
const confirmed = async (question) => {
|
|
@@ -3810,7 +3850,7 @@ async function handleHandoff(subArgs, injectedApi, confirm2 = askYesNo) {
|
|
|
3810
3850
|
return;
|
|
3811
3851
|
}
|
|
3812
3852
|
case "merge": {
|
|
3813
|
-
await confirmed((group ? `Merge ${branch} into
|
|
3853
|
+
await confirmed((group ? `Merge ${branch} into ${(0, import_core10.mergeTarget)(handoff)}? Nothing is merged unless every repository can take it${(0, import_core10.checkoutRule)(handoff) ? " and is on that branch" : ""}; once a finished run has merged everything, its worktrees and branches are removed.` : `Merge ${branch} into ${(0, import_core10.mergeTarget)(handoff)}?${(0, import_core10.checkoutRule)(handoff)} Once a finished run has merged everything, its worktrees and branches are removed.`) + repairedNotice(handoff));
|
|
3814
3854
|
const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
|
|
3815
3855
|
if (ok) console.log(message);
|
|
3816
3856
|
else fail(message);
|
|
@@ -4107,21 +4147,23 @@ async function handleApprove(subArgs, injectedApi) {
|
|
|
4107
4147
|
const sessionId = resolveSessionId(subArgs);
|
|
4108
4148
|
const api = await connect(subArgs, injectedApi);
|
|
4109
4149
|
await followExecution(api, sessionId, async () => {
|
|
4150
|
+
let approved;
|
|
4110
4151
|
try {
|
|
4111
|
-
await api.approveReview(sessionId);
|
|
4152
|
+
approved = await api.approveReview(sessionId);
|
|
4112
4153
|
} catch (err) {
|
|
4113
4154
|
console.error(`Failed to approve: ${err.message}`);
|
|
4114
4155
|
process.exit(1);
|
|
4115
4156
|
}
|
|
4116
|
-
console.error("Plan approved: continuing execution.");
|
|
4157
|
+
console.error(approved.message ?? "Plan approved: continuing execution.");
|
|
4117
4158
|
});
|
|
4118
4159
|
}
|
|
4119
4160
|
|
|
4120
4161
|
// src/commands/task-assign.ts
|
|
4121
4162
|
var import_core29 = require("@ordewell/core");
|
|
4122
|
-
var
|
|
4163
|
+
var import_plan_utils3 = require("@ordewell/core/plan-utils");
|
|
4123
4164
|
|
|
4124
4165
|
// src/tui/taskAssignment.ts
|
|
4166
|
+
var import_plan_utils = require("@ordewell/core/plan-utils");
|
|
4125
4167
|
function runnerServes(runner, model) {
|
|
4126
4168
|
return !runner || !model.runners || model.runners.length === 0 || model.runners.includes(runner);
|
|
4127
4169
|
}
|
|
@@ -4144,7 +4186,7 @@ function effortsForTask(models, task) {
|
|
|
4144
4186
|
}
|
|
4145
4187
|
function assignedModelFor(model, currentEffort) {
|
|
4146
4188
|
const variants = model.variants ?? [];
|
|
4147
|
-
const thinkingEffort =
|
|
4189
|
+
const thinkingEffort = (0, import_plan_utils.clampThinkingEffort)(currentEffort, variants);
|
|
4148
4190
|
return {
|
|
4149
4191
|
modelId: model.id,
|
|
4150
4192
|
modelLabel: model.label,
|
|
@@ -4158,6 +4200,7 @@ function modesForTask(modesByRunner, task) {
|
|
|
4158
4200
|
}
|
|
4159
4201
|
|
|
4160
4202
|
// src/tui/state.ts
|
|
4203
|
+
var import_plan_utils2 = require("@ordewell/core/plan-utils");
|
|
4161
4204
|
var import_core28 = require("@ordewell/core");
|
|
4162
4205
|
|
|
4163
4206
|
// src/tui/editor.ts
|
|
@@ -4354,12 +4397,7 @@ function resumesTask(task) {
|
|
|
4354
4397
|
return task?.status === "awaiting_user" && task.awaitingReason === "usage-limit";
|
|
4355
4398
|
}
|
|
4356
4399
|
function findTask(tasks, id) {
|
|
4357
|
-
|
|
4358
|
-
if (task.id === id) return task;
|
|
4359
|
-
const found = findTask(task.subtasks ?? [], id);
|
|
4360
|
-
if (found) return found;
|
|
4361
|
-
}
|
|
4362
|
-
return void 0;
|
|
4400
|
+
return (0, import_plan_utils2.taskPlacement)(tasks, id)?.task;
|
|
4363
4401
|
}
|
|
4364
4402
|
function refFor(state, task) {
|
|
4365
4403
|
return (0, import_core28.taskRefOf)(state.tasks, task.id) ?? (0, import_core28.taskRef)(task);
|
|
@@ -4426,6 +4464,7 @@ function initialState(overrides = {}) {
|
|
|
4426
4464
|
workspace: process.cwd(),
|
|
4427
4465
|
handoff: null,
|
|
4428
4466
|
runMerge: null,
|
|
4467
|
+
plannerResend: null,
|
|
4429
4468
|
overlay: null,
|
|
4430
4469
|
pendingApprovals: [],
|
|
4431
4470
|
queuedPrompts: import_core28.EMPTY_HOLD,
|
|
@@ -4444,8 +4483,8 @@ function initialState(overrides = {}) {
|
|
|
4444
4483
|
}
|
|
4445
4484
|
|
|
4446
4485
|
// src/commands/task-assign.ts
|
|
4447
|
-
var refOf = (tasks, task) => (0,
|
|
4448
|
-
var namedOf = (tasks, task) => (0,
|
|
4486
|
+
var refOf = (tasks, task) => (0, import_plan_utils3.taskRefOf)(tasks, task.id) ?? (0, import_plan_utils3.taskRef)(task);
|
|
4487
|
+
var namedOf = (tasks, task) => (0, import_plan_utils3.titledTaskRefOf)(tasks, task.id) ?? (0, import_plan_utils3.titledTaskRef)(task);
|
|
4449
4488
|
async function withTask(subArgs, usage, injectedApi, run, { joinValue = false } = {}) {
|
|
4450
4489
|
await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId, plan) => {
|
|
4451
4490
|
const tasks = taskViews(plan);
|
|
@@ -4507,8 +4546,9 @@ Model \xB7 ${namedOf(tasks, task)}${task.assignedRunner ? ` (${task.assignedRunn
|
|
|
4507
4546
|
const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
|
|
4508
4547
|
await api.updateTask(sessionId, task.id, { assignedModel });
|
|
4509
4548
|
console.log(`Task ${refOf(tasks, task)} model set to ${model.label}.`);
|
|
4510
|
-
|
|
4511
|
-
|
|
4549
|
+
const was = task.assignedModel?.thinkingEffort;
|
|
4550
|
+
if (was && assignedModel.thinkingEffort !== was) {
|
|
4551
|
+
console.log(` ${model.label} does not expose "${was}", so the effort is now ${assignedModel.thinkingEffort ? `"${assignedModel.thinkingEffort}", its nearest level` : "the runner default"}.`);
|
|
4512
4552
|
}
|
|
4513
4553
|
});
|
|
4514
4554
|
}
|
|
@@ -4584,15 +4624,16 @@ Mode \xB7 ${namedOf(tasks, task)}${task.assignedRunner ? ` (${task.assignedRunne
|
|
|
4584
4624
|
var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
|
|
4585
4625
|
async function handleTaskDeps(subArgs, injectedApi) {
|
|
4586
4626
|
await withTask(subArgs, DEPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
|
|
4587
|
-
const candidates = (0,
|
|
4627
|
+
const candidates = (0, import_plan_utils3.dependencyCandidates)(tasks, task.id);
|
|
4628
|
+
const held = task.dependencies.filter((id) => !candidates.some((c) => c.id === id)).flatMap((id) => (0, import_plan_utils3.resolveTaskRef)(tasks, id) ?? []);
|
|
4588
4629
|
if (!value) {
|
|
4589
4630
|
console.log(`
|
|
4590
4631
|
Dependencies \xB7 ${namedOf(tasks, task)}
|
|
4591
4632
|
`);
|
|
4592
4633
|
if (candidates.length === 0) {
|
|
4593
|
-
console.log(`
|
|
4634
|
+
console.log(` No earlier siblings to offer as dependencies for ${refOf(tasks, task)}.`);
|
|
4594
4635
|
}
|
|
4595
|
-
for (const c of candidates) {
|
|
4636
|
+
for (const c of [...candidates, ...held]) {
|
|
4596
4637
|
const chosen = task.dependencies.includes(c.id);
|
|
4597
4638
|
console.log(` ${chosen ? "*" : " "} ${c.id} ${namedOf(tasks, c)}${c.status === "completed" ? " (already completed)" : ""}`);
|
|
4598
4639
|
}
|
|
@@ -4600,8 +4641,8 @@ Dependencies \xB7 ${namedOf(tasks, task)}
|
|
|
4600
4641
|
* = current. ${DEPS_USAGE}`);
|
|
4601
4642
|
return;
|
|
4602
4643
|
}
|
|
4603
|
-
const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) =>
|
|
4604
|
-
const check = (0,
|
|
4644
|
+
const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) => (0, import_plan_utils3.resolveUserTaskRef)(tasks, token)?.id ?? token);
|
|
4645
|
+
const check = (0, import_plan_utils3.canSetDependencies)(tasks, task.id, dependencies);
|
|
4605
4646
|
if (!check.ok) fail(check.error ?? "Invalid dependencies.");
|
|
4606
4647
|
await api.updateTask(sessionId, task.id, { dependencies });
|
|
4607
4648
|
console.log(dependencies.length > 0 ? `Task ${refOf(tasks, task)} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task ${refOf(tasks, task)} no longer depends on anything.`);
|
|
@@ -4666,6 +4707,7 @@ var SLASH_COMMANDS = [
|
|
|
4666
4707
|
{ name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
|
|
4667
4708
|
{ name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
|
|
4668
4709
|
{ name: "stop", usage: "/stop", description: "Stop the planner or the running execution, whichever is active", category: "planning" },
|
|
4710
|
+
{ name: "resend", usage: "/resend cancel", description: "Keep what waits on the planner's usage limit from being sent at the reset: it goes with your next message", category: "planning" },
|
|
4669
4711
|
{ name: "handoff", usage: "/handoff [review|merge|discard|cleanup]", description: "Land an isolated run's branch: review it, merge it into yours, or discard it", category: "planning" },
|
|
4670
4712
|
// Tasks
|
|
4671
4713
|
{ name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
|
|
@@ -4675,7 +4717,7 @@ var SLASH_COMMANDS = [
|
|
|
4675
4717
|
{ name: "skip", usage: "/skip <id>", description: "Skip a task", category: "tasks" },
|
|
4676
4718
|
{ name: "retry", usage: "/retry <id>", description: "Re-run a failed task", category: "tasks" },
|
|
4677
4719
|
{ name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
|
|
4678
|
-
{ name: "force-start", usage: "/force-start <id>", description: "
|
|
4720
|
+
{ name: "force-start", usage: "/force-start <id>", description: "Approve a task and start it now, or once its dependencies finish", category: "tasks" },
|
|
4679
4721
|
{ name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
|
|
4680
4722
|
{ name: "open", usage: "/open <id>", description: "Open a task's view: its log, and the keys that answer its checkpoint or tool request", category: "tasks" },
|
|
4681
4723
|
{ name: "checkpoint", usage: "/checkpoint <id> approve|reject [reason]", description: "Answer the checkpoint a task waits at: approve lets it go on, reject tells it why not", category: "tasks" },
|
|
@@ -4829,7 +4871,7 @@ var import_core33 = require("@ordewell/core");
|
|
|
4829
4871
|
|
|
4830
4872
|
// src/tui/reducers/pickers.ts
|
|
4831
4873
|
var import_core31 = require("@ordewell/core");
|
|
4832
|
-
var
|
|
4874
|
+
var import_plan_utils4 = require("@ordewell/core/plan-utils");
|
|
4833
4875
|
var DEFAULT_EFFORT = "__runner_default__";
|
|
4834
4876
|
function picker(title, items, action, extra = {}) {
|
|
4835
4877
|
return { title, items, filter: "", index: 0, multi: false, chosen: [], filterable: false, action, ...extra };
|
|
@@ -4841,6 +4883,15 @@ function refillPicker(state, kinds) {
|
|
|
4841
4883
|
const hint = overlay.picker.action.kind === "set-model" ? providerErrorHint(state) : overlay.picker.hint;
|
|
4842
4884
|
return { ...state, overlay: { kind: "picker", picker: { ...overlay.picker, items, hint, index: 0 } } };
|
|
4843
4885
|
}
|
|
4886
|
+
function reseedRunnerChoice(before, after) {
|
|
4887
|
+
const overlay = after.overlay;
|
|
4888
|
+
if (overlay?.kind !== "picker" || overlay.picker.action.kind !== "set-runners") return after;
|
|
4889
|
+
const enabledIds = (state) => state.runners.filter((r) => r.enabled).map((r) => r.id);
|
|
4890
|
+
const shown = enabledIds(before);
|
|
4891
|
+
const untouched = overlay.picker.chosen.length === shown.length && shown.every((id) => overlay.picker.chosen.includes(id));
|
|
4892
|
+
const picker2 = { ...overlay.picker, items: pickerItemsFor(after, overlay.picker.action) };
|
|
4893
|
+
return { ...after, overlay: { kind: "picker", picker: untouched ? { ...picker2, chosen: enabledIds(after) } : picker2 } };
|
|
4894
|
+
}
|
|
4844
4895
|
function modelDetail(m) {
|
|
4845
4896
|
return [m.provider, m.pricing].filter(Boolean).join(" \xB7 ") || void 0;
|
|
4846
4897
|
}
|
|
@@ -4929,7 +4980,7 @@ function pickerItemsFor(state, action) {
|
|
|
4929
4980
|
}));
|
|
4930
4981
|
}
|
|
4931
4982
|
if (action.kind === "set-task-deps") {
|
|
4932
|
-
return (0,
|
|
4983
|
+
return (0, import_plan_utils4.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
|
|
4933
4984
|
id: candidate.id,
|
|
4934
4985
|
label: titledRefFor(state, candidate),
|
|
4935
4986
|
detail: candidate.status === "completed" ? "already completed" : void 0
|
|
@@ -5039,7 +5090,13 @@ var PLAN_KEYS = {
|
|
|
5039
5090
|
deps: { keys: "D", meaning: "pick the tasks it depends on", group: "Assign" },
|
|
5040
5091
|
skills: { keys: "K", meaning: "pick the skills it loads", group: "Assign" },
|
|
5041
5092
|
ops: { keys: "O", meaning: "switch between a change and an ops task", group: "Assign" },
|
|
5042
|
-
start: {
|
|
5093
|
+
start: {
|
|
5094
|
+
keys: "f",
|
|
5095
|
+
meaning: "start the task, approving it if it waits for you; it starts once its dependencies finish",
|
|
5096
|
+
group: "Run",
|
|
5097
|
+
// Added after the plan was approved, it waits for this key; the row says the same.
|
|
5098
|
+
hint: (task) => task && (0, import_core32.taskRowView)(task).waitsForApproval ? "f approve" : "f start"
|
|
5099
|
+
},
|
|
5043
5100
|
runPlan: { keys: "E", meaning: "run the whole plan", group: "Run", hint: "E run plan" },
|
|
5044
5101
|
stop: { keys: "S", meaning: "stop the run", group: "Run", hint: "S stop" },
|
|
5045
5102
|
cancel: {
|
|
@@ -5295,11 +5352,18 @@ function chatLayout(state, rows, cols) {
|
|
|
5295
5352
|
const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
|
|
5296
5353
|
const content = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
|
|
5297
5354
|
const taskHeader = tv ? taskHeaderLines(state, tv, cols) : [];
|
|
5298
|
-
const usage = rows <= 1 ? [] : tv ? taskUsageFooter(state, tv, blocks2, cols) : tokenRow(blocks2, cols);
|
|
5355
|
+
const usage = rows <= 1 ? [] : tv ? taskUsageFooter(state, tv, blocks2, cols) : [...plannerResendRow(state, cols), ...tokenRow(blocks2, cols)];
|
|
5299
5356
|
const footer = usage.slice(0, Math.max(1, rows - taskHeader.length - 1));
|
|
5300
5357
|
const space = Math.max(0, rows - taskHeader.length - footer.length);
|
|
5301
5358
|
return { lines: content, header: taskHeader, footer, anchor: content.length > space ? "bottom" : "top", maxScroll: Math.max(0, content.length - space) };
|
|
5302
5359
|
}
|
|
5360
|
+
function plannerResendRow(state, cols) {
|
|
5361
|
+
const { plannerResend } = state;
|
|
5362
|
+
const at = plannerResend ? new Date(plannerResend.at) : null;
|
|
5363
|
+
if (!plannerResend || !at || Number.isNaN(at.getTime())) return [];
|
|
5364
|
+
const whose = plannerResend.runner ? `${plannerResend.runner}'s` : "the planner's";
|
|
5365
|
+
return [style.yellow(truncate(`\u23F8 Resending at ${(0, import_core34.resumeTime)(at)}, once ${whose} usage limit resets \xB7 /resend cancel keeps it for your next message`, cols))];
|
|
5366
|
+
}
|
|
5303
5367
|
var tokenRow = (blocks2, cols) => {
|
|
5304
5368
|
const line = tokenLine(blocks2, cols);
|
|
5305
5369
|
return line ? [line] : [];
|
|
@@ -5307,15 +5371,13 @@ var tokenRow = (blocks2, cols) => {
|
|
|
5307
5371
|
function taskUsageFooter(state, tv, blocks2, cols) {
|
|
5308
5372
|
const task = findTask(state.tasks, tv.taskId);
|
|
5309
5373
|
const reported = task?.attemptUsage ?? [];
|
|
5374
|
+
if (!task || reported.length === 0) return tokenRow(blocks2, cols);
|
|
5310
5375
|
const onScreen = tv.attempt > 0 ? reported.find((a) => a.attempt === tv.attempt) : reported.at(-1);
|
|
5311
|
-
|
|
5312
|
-
const
|
|
5313
|
-
if (
|
|
5314
|
-
const
|
|
5315
|
-
return [
|
|
5316
|
-
style.grey(truncate(taskUsageText(usage), cols)),
|
|
5317
|
-
...models.length > 1 ? models.map((m) => style.grey(truncate(`${sanitize(m.model ?? "unattributed")}: ${usageMeasures(m.totals, true).join(" \xB7 ")}`, cols))) : []
|
|
5318
|
-
];
|
|
5376
|
+
const spend = (0, import_core34.taskUsageSummary)(task);
|
|
5377
|
+
const total = taskUsageText({ ...spend, attempt: reported.at(-1)?.attempt ?? 0 });
|
|
5378
|
+
if (onScreen === reported.at(-1)) return [style.grey(truncate(total, cols))];
|
|
5379
|
+
const own = onScreen ? [style.grey(truncate(`attempt ${onScreen.attempt}: ${taskUsageText({ totals: onScreen.totals, contextFill: (0, import_core34.plannerContextFill)(onScreen), attempt: onScreen.attempt })}`, cols))] : tokenRow(blocks2, cols);
|
|
5380
|
+
return [...own, style.grey(truncate(`task total: ${total}`, cols))];
|
|
5319
5381
|
}
|
|
5320
5382
|
function chatScrollMax(state) {
|
|
5321
5383
|
return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
|
|
@@ -5558,9 +5620,17 @@ function planScrollExtent(state) {
|
|
|
5558
5620
|
}
|
|
5559
5621
|
return planLayout(state, bodyRows(state), cols);
|
|
5560
5622
|
}
|
|
5623
|
+
function modelsByRunnerOf(models) {
|
|
5624
|
+
const out = {};
|
|
5625
|
+
for (const m of models) {
|
|
5626
|
+
for (const runner of m.runners ?? []) (out[runner] ??= []).push({ modelId: m.id, modelLabel: m.label, variants: [] });
|
|
5627
|
+
}
|
|
5628
|
+
return out;
|
|
5629
|
+
}
|
|
5561
5630
|
function taskLines(state, planRow, index, cols) {
|
|
5562
5631
|
const { task, parent } = planRow;
|
|
5563
|
-
const
|
|
5632
|
+
const runnerScope = state.runners.length > 0 ? { enabledRunners: state.runners.filter((r) => r.enabled).map((r) => r.id), allowlist: state.allowlist, models: modelsByRunnerOf(state.models) } : void 0;
|
|
5633
|
+
const row = (0, import_core34.taskRowView)(task, { parent, tasks: state.tasks, modes: modesForTask(state.modesByRunner, task), runnerScope });
|
|
5564
5634
|
const selected = state.focus === "plan" && index === state.selectedTask;
|
|
5565
5635
|
const expanded = state.expandedTaskId === task.id;
|
|
5566
5636
|
const rawIcon = row.status === "running" ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[row.status];
|
|
@@ -5582,8 +5652,7 @@ function taskLines(state, planRow, index, cols) {
|
|
|
5582
5652
|
const prefix = lineIndex === 0 ? head : continuation;
|
|
5583
5653
|
return prefix + (selected ? style.bold(line) : line);
|
|
5584
5654
|
});
|
|
5585
|
-
const
|
|
5586
|
-
const model = reported.length ? reported.join(", ") : task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
|
|
5655
|
+
const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
|
|
5587
5656
|
const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
|
|
5588
5657
|
const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
|
|
5589
5658
|
const runner = task.assignedRunner ?? "";
|
|
@@ -5599,36 +5668,39 @@ function taskLines(state, planRow, index, cols) {
|
|
|
5599
5668
|
if (row.conflict) {
|
|
5600
5669
|
const where = row.conflict.repo ? ` in ${row.conflict.repo}` : "";
|
|
5601
5670
|
const files = row.conflict.files ? ` (${row.conflict.files})` : "";
|
|
5602
|
-
lines.push(style.red
|
|
5671
|
+
lines.push(...statusLines(style.red, "\u26A0", `merge conflict${where}${files}: its work is kept on its own branch`, cols, bodyPad));
|
|
5603
5672
|
}
|
|
5604
5673
|
if (row.repairing) {
|
|
5605
5674
|
const files = row.repairing.files ? ` in ${row.repairing.files}` : "";
|
|
5606
5675
|
const attempt = row.repairing.attempt ? ` (attempt ${row.repairing.attempt.attempt}/${row.repairing.attempt.limit})` : "";
|
|
5607
|
-
lines.push(style.yellow
|
|
5676
|
+
lines.push(...statusLines(style.yellow, "\u21BB", `repairing conflict${files}${attempt}`, cols, bodyPad));
|
|
5608
5677
|
}
|
|
5609
5678
|
if (row.mergeGate) {
|
|
5610
|
-
lines.push(style.yellow
|
|
5679
|
+
lines.push(...statusLines(style.yellow, "\u23F8", `waits for Merge all: ${row.mergeGate.join(", ")} not merged into your branch yet`, cols, bodyPad));
|
|
5680
|
+
}
|
|
5681
|
+
if (row.assignmentFlag) {
|
|
5682
|
+
lines.push(...statusLines(style.yellow, "\u26A0", assignmentFlagText(row), cols, bodyPad));
|
|
5611
5683
|
}
|
|
5612
5684
|
if (row.waitsForApproval) {
|
|
5613
|
-
lines.push(style.yellow
|
|
5685
|
+
lines.push(...statusLines(style.yellow, "\u23F8", `${inRow(row.waitsForApproval)} \xB7 f approves it \xB7 /run approves all`, cols, bodyPad));
|
|
5614
5686
|
}
|
|
5615
5687
|
if (row.blockedBy) {
|
|
5616
|
-
lines.push(style.red
|
|
5688
|
+
lines.push(...statusLines(style.red, "\u2298", `${inRow((0, import_core34.blockedByLabel)(row.blockedBy))}: runs once ${row.blockedBy.length === 1 ? "it is retried and passes" : "they are retried and pass"}`, cols, bodyPad));
|
|
5617
5689
|
}
|
|
5618
5690
|
if (row.status === "skipped") {
|
|
5619
|
-
lines.push(style.grey
|
|
5691
|
+
lines.push(...statusLines(style.grey, "\xBB", `${inRow((0, import_core34.skippedLabel)(row.skipReason))}: nothing of it landed; m done lands its kept work`, cols, bodyPad));
|
|
5620
5692
|
}
|
|
5621
5693
|
const resumable = row.actions.includes("resume-now");
|
|
5622
5694
|
if (row.resumes) {
|
|
5623
|
-
lines.push(style.yellow
|
|
5695
|
+
lines.push(...statusLines(style.yellow, "\u23F8", `${inRow(row.resumes)}, once the usage limit resets: ${resumable ? "r resume now \xB7 " : ""}c don't resume`, cols, bodyPad));
|
|
5624
5696
|
} else if (resumable) {
|
|
5625
|
-
lines.push(style.yellow
|
|
5697
|
+
lines.push(...statusLines(style.yellow, "\u23F8", "waits for the usage limit to reset: r resume now", cols, bodyPad));
|
|
5626
5698
|
}
|
|
5627
5699
|
if (row.opsChangedFiles) {
|
|
5628
|
-
lines.push(style.yellow
|
|
5700
|
+
lines.push(...statusLines(style.yellow, "\u26A0", "an ops task changed tracked files: check them, then m done or /retry", cols, bodyPad));
|
|
5629
5701
|
}
|
|
5630
5702
|
if (row.repaired?.landed) {
|
|
5631
|
-
lines.push(style.grey
|
|
5703
|
+
lines.push(...statusLines(style.grey, "\u21BB", `landed after repairing conflict in ${row.repaired.files}`, cols, bodyPad));
|
|
5632
5704
|
}
|
|
5633
5705
|
let editorLine;
|
|
5634
5706
|
if (expanded) {
|
|
@@ -5670,6 +5742,11 @@ function taskLines(state, planRow, index, cols) {
|
|
|
5670
5742
|
}
|
|
5671
5743
|
return { lines, editorLine };
|
|
5672
5744
|
}
|
|
5745
|
+
function statusLines(paint, glyph, text, cols, pad2) {
|
|
5746
|
+
const lead = `${pad2}${glyph} `;
|
|
5747
|
+
const room = Math.max(1, cols - width(lead));
|
|
5748
|
+
return wrap(text, room).map((line, i) => paint(`${i === 0 ? lead : " ".repeat(width(lead))}${line}`));
|
|
5749
|
+
}
|
|
5673
5750
|
function taskText(label, text, cols, pad2 = " ") {
|
|
5674
5751
|
const room = Math.max(1, cols - width(pad2));
|
|
5675
5752
|
return [
|
|
@@ -6057,7 +6134,7 @@ function renderHandoff(state, overlay, rows, cols) {
|
|
|
6057
6134
|
const caret = active ? style.brand("\u276F") : " ";
|
|
6058
6135
|
return `${caret} ${active ? style.bold(action.label) : action.label}${style.grey(`: ${action.hint}`)}`;
|
|
6059
6136
|
});
|
|
6060
|
-
const perRepo = isRepoGroup(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
|
|
6137
|
+
const perRepo = (0, import_core10.isRepoGroup)(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
|
|
6061
6138
|
return frame(
|
|
6062
6139
|
"Run handoff",
|
|
6063
6140
|
[
|
|
@@ -6574,12 +6651,7 @@ function openTaskView(state, sessionId, taskId) {
|
|
|
6574
6651
|
if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
|
|
6575
6652
|
const taskView = {
|
|
6576
6653
|
taskId,
|
|
6577
|
-
|
|
6578
|
-
attempts: [],
|
|
6579
|
-
attempt: 0,
|
|
6580
|
-
pending: [],
|
|
6581
|
-
loaded: false,
|
|
6582
|
-
followLatest: true,
|
|
6654
|
+
...import_core40.EMPTY_TASK_LOG_FOLLOW,
|
|
6583
6655
|
queuedIndex: null
|
|
6584
6656
|
};
|
|
6585
6657
|
return step({ ...state, taskView, focus: "chat", scroll: 0 }, [{ type: "openTaskLog", sessionId, taskId }]);
|
|
@@ -6595,67 +6667,35 @@ function clampQueueIndex(tv) {
|
|
|
6595
6667
|
const max = tv.view.queued.length - 1;
|
|
6596
6668
|
return tv.queuedIndex <= max ? tv : { ...tv, queuedIndex: max };
|
|
6597
6669
|
}
|
|
6598
|
-
function applyLive(tv, attempt, events) {
|
|
6599
|
-
if (attempt <= 0) return tv;
|
|
6600
|
-
let next = tv;
|
|
6601
|
-
if (attempt > next.attempt) {
|
|
6602
|
-
if (!next.followLatest) {
|
|
6603
|
-
return next.attempts.includes(attempt) ? next : { ...next, attempts: [...next.attempts, attempt] };
|
|
6604
|
-
}
|
|
6605
|
-
next = {
|
|
6606
|
-
...next,
|
|
6607
|
-
view: import_core40.EMPTY_TASK_LOG,
|
|
6608
|
-
attempt,
|
|
6609
|
-
attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
|
|
6610
|
-
};
|
|
6611
|
-
}
|
|
6612
|
-
if (attempt < next.attempt) return next;
|
|
6613
|
-
return clampQueueIndex({ ...next, view: events.reduce(import_core40.reduceTaskLog, next.view) });
|
|
6614
|
-
}
|
|
6615
6670
|
function taskLogAbandoned(state) {
|
|
6616
6671
|
const tv = state.taskView;
|
|
6617
6672
|
if (!tv || tv.loaded) return state;
|
|
6618
|
-
|
|
6619
|
-
for (const batch of tv.pending) next = applyLive(next, batch.attempt, batch.events);
|
|
6620
|
-
return { ...state, taskView: clampQueueIndex(next) };
|
|
6673
|
+
return { ...state, taskView: clampQueueIndex({ ...tv, ...(0, import_core40.followTaskLog)(tv, { type: "loaded", attempt: tv.attempt, events: [], abandoned: true }) }) };
|
|
6621
6674
|
}
|
|
6622
6675
|
function taskLogArrived(state, action) {
|
|
6623
6676
|
const tv = state.taskView;
|
|
6624
6677
|
if (!tv || tv.taskId !== action.taskId) return state;
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
}
|
|
6628
|
-
return { ...state, taskView: applyLive(tv, action.attempt, action.events) };
|
|
6678
|
+
const next = (0, import_core40.followTaskLog)(tv, { ...action, type: "live" });
|
|
6679
|
+
return { ...state, taskView: next === tv ? tv : clampQueueIndex({ ...tv, ...next }) };
|
|
6629
6680
|
}
|
|
6630
6681
|
function announceApprovals(state, action) {
|
|
6631
6682
|
if (state.taskView?.taskId === action.taskId) return state;
|
|
6632
6683
|
const task = findTask(state.tasks, action.taskId);
|
|
6633
|
-
const way = task ? `tab to the plan, select task ${task
|
|
6634
|
-
const spoken = action.events.reduce((s, event) => event.type === "approval_requested" && !event.decided ? say(s, "system", `Task ${task
|
|
6684
|
+
const way = task ? `tab to the plan, select task ${refFor(state, task)} and press enter (or /open ${refFor(state, task)})` : "tab to the plan, select it and press enter";
|
|
6685
|
+
const spoken = action.events.reduce((s, event) => event.type === "approval_requested" && !event.decided ? say(s, "system", `Task ${task ? refFor(state, task) : "?"} waits for approval: ${(0, import_core40.runnerToolSubject)(event.tool, event.args)}: ${way}, then ctrl-y allows or ctrl-g denies`) : s, state);
|
|
6635
6686
|
return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
|
|
6636
6687
|
}
|
|
6637
6688
|
function announceCheckpoint(state, action) {
|
|
6638
6689
|
const task = findTask(state.tasks, action.taskId) ?? state.tasks.find((t) => t.title === action.title);
|
|
6639
|
-
const label = task ? `Task ${task
|
|
6640
|
-
const where = task ? `tab to the plan, select task ${task
|
|
6690
|
+
const label = task ? `Task ${refFor(state, task)}` : action.title;
|
|
6691
|
+
const where = task ? `tab to the plan, select task ${refFor(state, task)} and press enter (or /open ${refFor(state, task)}) to answer` : "tab to the plan, select it and press enter to answer";
|
|
6641
6692
|
const spoken = say(state, "system", `${label} asks: ${(0, import_core40.truncateCheckpointSummary)(action.summary)}: ${where}`);
|
|
6642
6693
|
return { ...spoken, scroll: state.scroll };
|
|
6643
6694
|
}
|
|
6644
6695
|
function taskLogLoaded(state, action) {
|
|
6645
6696
|
const tv = state.taskView;
|
|
6646
6697
|
if (!tv || tv.taskId !== action.taskId) return state;
|
|
6647
|
-
|
|
6648
|
-
...tv,
|
|
6649
|
-
view: (0, import_core40.replayTaskLog)(action.events),
|
|
6650
|
-
attempts: action.attempts ?? tv.attempts,
|
|
6651
|
-
attempt: action.attempt
|
|
6652
|
-
} : { ...tv, attempts: action.attempts ?? tv.attempts };
|
|
6653
|
-
let next = { ...loaded, pending: [], loaded: true };
|
|
6654
|
-
for (const batch of tv.pending) {
|
|
6655
|
-
if (batch.attempt === action.attempt) continue;
|
|
6656
|
-
next = applyLive(next, batch.attempt, batch.events);
|
|
6657
|
-
}
|
|
6658
|
-
return { ...state, taskView: clampQueueIndex(next) };
|
|
6698
|
+
return { ...state, taskView: clampQueueIndex({ ...tv, ...(0, import_core40.followTaskLog)(tv, { ...action, type: "loaded" }) }) };
|
|
6659
6699
|
}
|
|
6660
6700
|
function answerApproval(state, tv, sessionId, decision) {
|
|
6661
6701
|
const approval = waitingApproval(tv);
|
|
@@ -6699,7 +6739,7 @@ function handleTaskViewKey(state, key) {
|
|
|
6699
6739
|
const tv = state.taskView;
|
|
6700
6740
|
if (!tv || !state.sessionId) return null;
|
|
6701
6741
|
if (key.name === "alt-left" || key.name === "alt-right") {
|
|
6702
|
-
const index = tv.attempts.indexOf(tv.attempt);
|
|
6742
|
+
const index = tv.attempts.indexOf(tv.loadingAttempt ?? tv.attempt);
|
|
6703
6743
|
if (index < 0) return null;
|
|
6704
6744
|
const target = key.name === "alt-left" ? index - 1 : index + 1;
|
|
6705
6745
|
const attempt = tv.attempts[target];
|
|
@@ -6707,7 +6747,7 @@ function handleTaskViewKey(state, key) {
|
|
|
6707
6747
|
return step(say(state, "system", target < 0 ? "This is the first attempt." : "This is the latest attempt."));
|
|
6708
6748
|
}
|
|
6709
6749
|
return step(
|
|
6710
|
-
{ ...state, taskView: { ...tv,
|
|
6750
|
+
{ ...state, taskView: { ...tv, ...(0, import_core40.followTaskLog)(tv, { type: "picked", attempt }) } },
|
|
6711
6751
|
[{ type: "loadTaskAttempt", sessionId: state.sessionId, taskId: tv.taskId, attempt }]
|
|
6712
6752
|
);
|
|
6713
6753
|
}
|
|
@@ -6739,7 +6779,7 @@ function handleTaskViewKey(state, key) {
|
|
|
6739
6779
|
|
|
6740
6780
|
// src/tui/reducers/taskEdits.ts
|
|
6741
6781
|
var import_core41 = require("@ordewell/core");
|
|
6742
|
-
var
|
|
6782
|
+
var import_plan_utils5 = require("@ordewell/core/plan-utils");
|
|
6743
6783
|
function openTaskRunnerPicker(state, task) {
|
|
6744
6784
|
if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
|
|
6745
6785
|
const action = { kind: "set-task-runner", taskId: task.id };
|
|
@@ -6761,7 +6801,7 @@ function confirmRemoveTask(state, task) {
|
|
|
6761
6801
|
overlay: {
|
|
6762
6802
|
kind: "confirm",
|
|
6763
6803
|
title: `Remove ${titledRefFor(state, task)}?`,
|
|
6764
|
-
message: (0,
|
|
6804
|
+
message: (0, import_plan_utils5.dependentsNotice)((0, import_plan_utils5.dependentsOf)(state.tasks, task.id), state.tasks) ?? "This cannot be undone.",
|
|
6765
6805
|
action: { kind: "remove-task", taskId: task.id }
|
|
6766
6806
|
}
|
|
6767
6807
|
});
|
|
@@ -6785,7 +6825,7 @@ function confirmForceStartPastGate(state, task) {
|
|
|
6785
6825
|
overlay: {
|
|
6786
6826
|
kind: "confirm",
|
|
6787
6827
|
title: `Force start ${titledRefFor(state, task)}?`,
|
|
6788
|
-
message: (0,
|
|
6828
|
+
message: (0, import_plan_utils5.pastGateConfirmation)((0, import_plan_utils5.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
|
|
6789
6829
|
action: { kind: "force-start-gated", taskId: task.id }
|
|
6790
6830
|
}
|
|
6791
6831
|
});
|
|
@@ -6793,16 +6833,16 @@ function confirmForceStartPastGate(state, task) {
|
|
|
6793
6833
|
function openTaskDepsPicker(state, task) {
|
|
6794
6834
|
const action = { kind: "set-task-deps", taskId: task.id };
|
|
6795
6835
|
const items = pickerItemsFor(state, action);
|
|
6796
|
-
if (items.length === 0) return fail3(state, `
|
|
6836
|
+
if (items.length === 0) return fail3(state, `No earlier siblings to offer as dependencies for ${refFor(state, task)}.`);
|
|
6797
6837
|
return step({
|
|
6798
6838
|
...state,
|
|
6799
6839
|
overlay: {
|
|
6800
6840
|
kind: "picker",
|
|
6801
6841
|
picker: picker(`Depends on \xB7 ${titledRefFor(state, task)}`, items, action, {
|
|
6802
|
-
hint: "
|
|
6842
|
+
hint: "Offers earlier siblings; existing dependencies are kept.",
|
|
6803
6843
|
multi: true,
|
|
6804
6844
|
filterable: true,
|
|
6805
|
-
chosen: task.dependencies
|
|
6845
|
+
chosen: task.dependencies
|
|
6806
6846
|
})
|
|
6807
6847
|
}
|
|
6808
6848
|
});
|
|
@@ -7020,14 +7060,7 @@ function taskCommand(state, token, run) {
|
|
|
7020
7060
|
});
|
|
7021
7061
|
}
|
|
7022
7062
|
function resolveTaskId2(state, token) {
|
|
7023
|
-
|
|
7024
|
-
if (byId) return byId.id;
|
|
7025
|
-
const order = Number(token);
|
|
7026
|
-
if (Number.isInteger(order)) {
|
|
7027
|
-
const byOrder = state.tasks.find((t) => t.order === order);
|
|
7028
|
-
if (byOrder) return byOrder.id;
|
|
7029
|
-
}
|
|
7030
|
-
return null;
|
|
7063
|
+
return (0, import_plan_utils5.resolveUserTaskRef)(state.tasks, token)?.id ?? null;
|
|
7031
7064
|
}
|
|
7032
7065
|
|
|
7033
7066
|
// src/tui/reducers/planPane.ts
|
|
@@ -7272,28 +7305,6 @@ function foldRun(state, input) {
|
|
|
7272
7305
|
const run = (0, import_core44.reduceRun)(state.run, input);
|
|
7273
7306
|
return run === state.run ? state : withRun(state, run);
|
|
7274
7307
|
}
|
|
7275
|
-
function mapRunTasks(tasks, map) {
|
|
7276
|
-
return tasks.map((task) => ({ ...map(task), subtasks: mapRunTasks(task.subtasks, map) }));
|
|
7277
|
-
}
|
|
7278
|
-
function applyIsolationRecord(state, plan) {
|
|
7279
|
-
const record = isolationOfPlan(plan);
|
|
7280
|
-
if (!record) return state;
|
|
7281
|
-
const { run } = state;
|
|
7282
|
-
return withRun(state, {
|
|
7283
|
-
...run,
|
|
7284
|
-
tasks: mapRunTasks(run.tasks, (task) => ({ ...task, isolation: record.tasks[task.id] ?? { state: "none" } })),
|
|
7285
|
-
handoff: withDeferral(record.handoff, run.handoff)
|
|
7286
|
-
});
|
|
7287
|
-
}
|
|
7288
|
-
function clearRunIsolation(run) {
|
|
7289
|
-
return {
|
|
7290
|
-
...run,
|
|
7291
|
-
handoff: null,
|
|
7292
|
-
runMerge: null,
|
|
7293
|
-
mergeResult: null,
|
|
7294
|
-
tasks: mapRunTasks(run.tasks, ({ isolation: _dropped, ...task }) => task)
|
|
7295
|
-
};
|
|
7296
|
-
}
|
|
7297
7308
|
function applySettings(state, settings) {
|
|
7298
7309
|
return {
|
|
7299
7310
|
...state,
|
|
@@ -7593,6 +7604,8 @@ function runCommand(state, { name, args }) {
|
|
|
7593
7604
|
return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
|
|
7594
7605
|
case "rewind":
|
|
7595
7606
|
return rewind(state, args[0]);
|
|
7607
|
+
case "resend":
|
|
7608
|
+
return args[0] === "cancel" ? withSession(state, (sessionId) => step(state, [{ type: "cancelPlannerResend", sessionId }])) : step(say(state, "system", "A planner turn that hits the planner's usage limit is sent again at the reset: /resend cancel keeps it for your next message instead."));
|
|
7596
7609
|
case "compact":
|
|
7597
7610
|
return withIdlePlanner(
|
|
7598
7611
|
state,
|
|
@@ -7866,7 +7879,7 @@ function runners(state, args) {
|
|
|
7866
7879
|
multi: true,
|
|
7867
7880
|
chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
|
|
7868
7881
|
})
|
|
7869
|
-
} });
|
|
7882
|
+
} }, [{ type: "loadRunners" }]);
|
|
7870
7883
|
}
|
|
7871
7884
|
function setMaxParallel(state, arg) {
|
|
7872
7885
|
if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once: /parallel <n> changes it.`));
|
|
@@ -8162,6 +8175,24 @@ function reduceAction(state, action) {
|
|
|
8162
8175
|
// tasks — may carry over into this.
|
|
8163
8176
|
case "sessionStarted":
|
|
8164
8177
|
return step(withRun({ ...state, sessionId: action.sessionId, goal: action.goal, taskView: null }, import_core49.EMPTY_RUN));
|
|
8178
|
+
// A loaded session is a different one: the turn the old one had out, the
|
|
8179
|
+
// prompts held behind it and a stop armed against it are not this one's.
|
|
8180
|
+
// (`sessionStarted` cannot reset them: `startConversation` dispatches it
|
|
8181
|
+
// as its own turn begins.)
|
|
8182
|
+
case "sessionLoaded":
|
|
8183
|
+
return step(withRun({
|
|
8184
|
+
...state,
|
|
8185
|
+
sessionId: action.sessionId,
|
|
8186
|
+
goal: action.goal,
|
|
8187
|
+
taskView: null,
|
|
8188
|
+
planner: "idle",
|
|
8189
|
+
turnGate: import_core49.NO_TURN,
|
|
8190
|
+
pendingApprovals: [],
|
|
8191
|
+
queuedPrompts: import_core49.EMPTY_HOLD,
|
|
8192
|
+
stopArmed: false,
|
|
8193
|
+
forceSendPending: null,
|
|
8194
|
+
overlay: state.overlay?.kind === "approval" ? null : state.overlay
|
|
8195
|
+
}, import_core49.EMPTY_RUN));
|
|
8165
8196
|
// The daemon registers a session only once planning succeeds; when it does
|
|
8166
8197
|
// not, holding on to the id would send every next message to a session the
|
|
8167
8198
|
// server never had.
|
|
@@ -8171,6 +8202,7 @@ function reduceAction(state, action) {
|
|
|
8171
8202
|
sessionId: null,
|
|
8172
8203
|
goal: "",
|
|
8173
8204
|
taskView: null,
|
|
8205
|
+
turnGate: import_core49.NO_TURN,
|
|
8174
8206
|
pendingApprovals: [],
|
|
8175
8207
|
queuedPrompts: import_core49.EMPTY_HOLD,
|
|
8176
8208
|
stopArmed: false,
|
|
@@ -8184,6 +8216,9 @@ function reduceAction(state, action) {
|
|
|
8184
8216
|
case "chatRestored":
|
|
8185
8217
|
if (stale(state, action.sessionId)) return step(state);
|
|
8186
8218
|
return step({ ...state, conversation: (0, import_core49.fromTranscript)(action.history, action.researchLog, action.plannerUsage, action.pendingTurns), scroll: 0 });
|
|
8219
|
+
case "planRefreshed":
|
|
8220
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
8221
|
+
return step(showPlan(state, action.plan));
|
|
8187
8222
|
case "planUpdated": {
|
|
8188
8223
|
if (stale(state, action.sessionId)) return step(state);
|
|
8189
8224
|
const settled = { ...showPlan(state, action.plan), planner: "idle" };
|
|
@@ -8270,10 +8305,16 @@ function reduceAction(state, action) {
|
|
|
8270
8305
|
if (stale(state, action.sessionId)) return step(state);
|
|
8271
8306
|
return step(showDiff(state, action.diff));
|
|
8272
8307
|
}
|
|
8273
|
-
|
|
8308
|
+
// What Merge all did, folded the way the Session says it: a merge that
|
|
8309
|
+
// leaves the run going keeps the marks (ADR-0020), and only `run_closed`
|
|
8310
|
+
// takes them (below).
|
|
8311
|
+
case "isolationMerged": {
|
|
8274
8312
|
if (stale(state, action.sessionId)) return step(state);
|
|
8275
|
-
|
|
8276
|
-
|
|
8313
|
+
return step(withoutHandoffOverlay(foldRun(state, { type: "isolation_merge", result: action.result })));
|
|
8314
|
+
}
|
|
8315
|
+
case "runClosed": {
|
|
8316
|
+
if (stale(state, action.sessionId)) return step(state);
|
|
8317
|
+
return step(withoutHandoffOverlay(foldRun(state, { type: "run_closed" })));
|
|
8277
8318
|
}
|
|
8278
8319
|
case "executionComplete": {
|
|
8279
8320
|
if (stale(state, action.sessionId)) return step(state);
|
|
@@ -8333,9 +8374,9 @@ function reduceAction(state, action) {
|
|
|
8333
8374
|
if (stale(state, action.sessionId)) return step(state);
|
|
8334
8375
|
if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
|
|
8335
8376
|
return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
|
|
8336
|
-
// A fork holds no run, whatever the session it came from was
|
|
8337
|
-
// original keeps
|
|
8338
|
-
// arrives next, onto an empty run view.
|
|
8377
|
+
// A fork holds no run and no turn, whatever the session it came from was
|
|
8378
|
+
// doing — the original keeps both, and its events are stale from here on.
|
|
8379
|
+
// Its plan arrives next, onto an empty run view.
|
|
8339
8380
|
case "sessionForked":
|
|
8340
8381
|
return step(withRun({
|
|
8341
8382
|
...state,
|
|
@@ -8344,6 +8385,7 @@ function reduceAction(state, action) {
|
|
|
8344
8385
|
taskView: null,
|
|
8345
8386
|
status: "idle",
|
|
8346
8387
|
planner: "idle",
|
|
8388
|
+
turnGate: import_core49.NO_TURN,
|
|
8347
8389
|
planApproved: false,
|
|
8348
8390
|
pendingApprovals: [],
|
|
8349
8391
|
queuedPrompts: import_core49.EMPTY_HOLD,
|
|
@@ -8360,13 +8402,15 @@ function reduceAction(state, action) {
|
|
|
8360
8402
|
focus: "chat",
|
|
8361
8403
|
editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
|
|
8362
8404
|
});
|
|
8405
|
+
case "sharedSettingsChanged":
|
|
8406
|
+
return step(state, [{ type: "loadScope" }]);
|
|
8363
8407
|
case "runnersLoaded":
|
|
8364
|
-
return step(refreshStepper(refillPicker({
|
|
8408
|
+
return step(refreshStepper(reseedRunnerChoice(state, refillPicker({
|
|
8365
8409
|
...state,
|
|
8366
8410
|
runners: action.runners,
|
|
8367
8411
|
runnersKnown: true,
|
|
8368
8412
|
orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
|
|
8369
|
-
}, ["set-task-runner"])));
|
|
8413
|
+
}, ["set-task-runner"]))));
|
|
8370
8414
|
case "failed": {
|
|
8371
8415
|
if (!action.turn) return step(say(taskLogAbandoned(state), "error", action.message));
|
|
8372
8416
|
if (state.forceSendPending) {
|
|
@@ -8407,12 +8451,16 @@ function reduceAction(state, action) {
|
|
|
8407
8451
|
return step(disarmStop(state));
|
|
8408
8452
|
}
|
|
8409
8453
|
}
|
|
8454
|
+
function withoutHandoffOverlay(state) {
|
|
8455
|
+
return state.handoff || state.overlay?.kind !== "handoff" ? state : { ...state, overlay: null };
|
|
8456
|
+
}
|
|
8410
8457
|
function showPlan(state, plan) {
|
|
8411
|
-
const folded =
|
|
8458
|
+
const folded = foldRun(state, planSnapshot(plan));
|
|
8412
8459
|
const { tasks } = folded;
|
|
8413
8460
|
const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
|
|
8414
8461
|
const next = {
|
|
8415
8462
|
...folded,
|
|
8463
|
+
plannerResend: plannerResendOf(plan),
|
|
8416
8464
|
expandedTaskId,
|
|
8417
8465
|
taskEditor: expandedTaskId !== null ? state.taskEditor : null
|
|
8418
8466
|
};
|
|
@@ -8422,6 +8470,11 @@ function showPlan(state, plan) {
|
|
|
8422
8470
|
focus: tasks.length === 0 ? "chat" : state.focus
|
|
8423
8471
|
});
|
|
8424
8472
|
}
|
|
8473
|
+
function plannerResendOf(plan) {
|
|
8474
|
+
const saved = plan?.plannerResend;
|
|
8475
|
+
if (typeof saved?.at !== "string") return null;
|
|
8476
|
+
return { ...typeof saved.runner === "string" ? { runner: saved.runner } : {}, at: saved.at };
|
|
8477
|
+
}
|
|
8425
8478
|
function runStatus(state, tasks) {
|
|
8426
8479
|
if (tasks.some(import_core28.isTaskRunning)) return "executing";
|
|
8427
8480
|
if (tasks.some((t) => t.status === "awaiting_user") || state.runMerge?.paused) return "executing";
|
|
@@ -8760,6 +8813,7 @@ function createInbound(dispatch, sessionId) {
|
|
|
8760
8813
|
const delivered = new Delivered(256);
|
|
8761
8814
|
const copies = new Copies(64);
|
|
8762
8815
|
const shown = new WindowSet(64);
|
|
8816
|
+
const shownTexts = new WindowSet(64);
|
|
8763
8817
|
const pending = new WindowSet(16);
|
|
8764
8818
|
const DELTA_DEBOUNCE_MS = 75;
|
|
8765
8819
|
let held = null;
|
|
@@ -8788,7 +8842,8 @@ function createInbound(dispatch, sessionId) {
|
|
|
8788
8842
|
timer = setTimeout(flush, DELTA_DEBOUNCE_MS);
|
|
8789
8843
|
};
|
|
8790
8844
|
const showReply = (message) => {
|
|
8791
|
-
shown.add(message.content);
|
|
8845
|
+
shown.add(replyKey(message.content, message.timestamp));
|
|
8846
|
+
shownTexts.add(message.content);
|
|
8792
8847
|
dispatch({ type: "sessionMessage", message, sessionId });
|
|
8793
8848
|
};
|
|
8794
8849
|
const fresh = (id, event) => {
|
|
@@ -8860,14 +8915,15 @@ function createInbound(dispatch, sessionId) {
|
|
|
8860
8915
|
const id = `e${++subscription}`;
|
|
8861
8916
|
return (event) => receive("execution", id, event);
|
|
8862
8917
|
},
|
|
8863
|
-
backfill: (content, timestamp) => {
|
|
8864
|
-
if (shown.has(content)) return;
|
|
8865
|
-
const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
8918
|
+
backfill: (content, timestamp, runs) => {
|
|
8919
|
+
if (timestamp ? shown.has(replyKey(content, timestamp)) : shownTexts.has(content)) return;
|
|
8920
|
+
const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString(), ...runs ? { runs } : {} };
|
|
8866
8921
|
showReply(message);
|
|
8867
8922
|
if (timestamp) pending.add(replyKey(content, timestamp));
|
|
8868
8923
|
},
|
|
8869
8924
|
redrawn: (content, timestamp) => {
|
|
8870
|
-
shown.add(content);
|
|
8925
|
+
shown.add(replyKey(content, timestamp));
|
|
8926
|
+
shownTexts.add(content);
|
|
8871
8927
|
pending.add(replyKey(content, timestamp));
|
|
8872
8928
|
},
|
|
8873
8929
|
flush,
|
|
@@ -8939,7 +8995,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
|
|
|
8939
8995
|
// its only reader, and the reducer drops it unless that view is open on
|
|
8940
8996
|
// this task. The run's watcher itself does nothing with it.
|
|
8941
8997
|
case "task_log":
|
|
8942
|
-
dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, sessionId });
|
|
8998
|
+
dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, position: stampOf(event) ?? void 0, sessionId });
|
|
8943
8999
|
return;
|
|
8944
9000
|
case "task_updated":
|
|
8945
9001
|
dispatch({ type: "taskUpdated", taskId: String(event.taskId), changes: event.changes, sessionId });
|
|
@@ -8949,10 +9005,13 @@ function dispatchLifecycle(dispatch, event, sessionId) {
|
|
|
8949
9005
|
// does with it.
|
|
8950
9006
|
case "task_output":
|
|
8951
9007
|
return;
|
|
8952
|
-
// Its asker already has the words, from the merge request itself
|
|
8953
|
-
//
|
|
9008
|
+
// Its asker already has the words, from the merge request itself. Whether
|
|
9009
|
+
// the merge ended the run is the Session's to say, in `run_closed`.
|
|
8954
9010
|
case "isolation_merge":
|
|
8955
|
-
|
|
9011
|
+
dispatch({ type: "isolationMerged", result: event.result, sessionId });
|
|
9012
|
+
return;
|
|
9013
|
+
case "run_closed":
|
|
9014
|
+
dispatch({ type: "runClosed", sessionId });
|
|
8956
9015
|
return;
|
|
8957
9016
|
default: {
|
|
8958
9017
|
const unhandled = event;
|
|
@@ -9049,9 +9108,10 @@ async function perform(effect, deps) {
|
|
|
9049
9108
|
case "execute":
|
|
9050
9109
|
await withExecutionStream(deps, effect.sessionId, () => api.executePlan(effect.sessionId));
|
|
9051
9110
|
return;
|
|
9111
|
+
// The seam keeps its own line of a Stop in the timeline and the stream
|
|
9112
|
+
// announces the run's end, so this adds none (ADR-0029).
|
|
9052
9113
|
case "stopExecution":
|
|
9053
9114
|
await api.stopExecution(effect.sessionId);
|
|
9054
|
-
dispatch({ type: "notice", message: "Execution stopped." });
|
|
9055
9115
|
return;
|
|
9056
9116
|
case "cancelPlanning": {
|
|
9057
9117
|
const { cancelled } = await api.cancelPlanning(effect.sessionId);
|
|
@@ -9069,8 +9129,7 @@ async function perform(effect, deps) {
|
|
|
9069
9129
|
case "taskAction": {
|
|
9070
9130
|
const { sessionId, taskId, action } = effect;
|
|
9071
9131
|
if (action === "cancel-resume") {
|
|
9072
|
-
|
|
9073
|
-
dispatch({ type: "notice", message: cancelled ? "Resume cancelled: retry the task once the limit resets." : "That task had no resume scheduled." });
|
|
9132
|
+
await api.cancelResume(sessionId, taskId);
|
|
9074
9133
|
await refreshPlan(deps, sessionId);
|
|
9075
9134
|
return;
|
|
9076
9135
|
}
|
|
@@ -9128,19 +9187,19 @@ async function perform(effect, deps) {
|
|
|
9128
9187
|
// The saved log is read off disk (ADR-0018, P1) so the view opens with the
|
|
9129
9188
|
// task's history; the live `task_log` stream then catches it up in place.
|
|
9130
9189
|
case "openTaskLog": {
|
|
9131
|
-
const attempts = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
|
|
9190
|
+
const { attempts, position } = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
|
|
9132
9191
|
if (attempts.length === 0) {
|
|
9133
|
-
dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], sessionId: effect.sessionId });
|
|
9192
|
+
dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], position, sessionId: effect.sessionId });
|
|
9134
9193
|
return;
|
|
9135
9194
|
}
|
|
9136
9195
|
const attempt = attempts[attempts.length - 1];
|
|
9137
|
-
const
|
|
9138
|
-
dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts,
|
|
9196
|
+
const log = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
|
|
9197
|
+
dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, ...log, sessionId: effect.sessionId });
|
|
9139
9198
|
return;
|
|
9140
9199
|
}
|
|
9141
9200
|
case "loadTaskAttempt": {
|
|
9142
|
-
const
|
|
9143
|
-
dispatch({ type: "taskLogLoaded", taskId: effect.taskId,
|
|
9201
|
+
const log = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
|
|
9202
|
+
dispatch({ type: "taskLogLoaded", taskId: effect.taskId, ...log, sessionId: effect.sessionId });
|
|
9144
9203
|
return;
|
|
9145
9204
|
}
|
|
9146
9205
|
// The daemon queues the message (or delivers it to a waiting task) and
|
|
@@ -9160,24 +9219,20 @@ async function perform(effect, deps) {
|
|
|
9160
9219
|
await refreshPlan(deps, sessionId);
|
|
9161
9220
|
return;
|
|
9162
9221
|
}
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9222
|
+
// One already delivered is refused in the daemon's words.
|
|
9223
|
+
case "removeTaskMessage":
|
|
9224
|
+
await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
|
|
9166
9225
|
return;
|
|
9167
|
-
}
|
|
9168
9226
|
// The task log shows the forced message at the head of the queue, then
|
|
9169
9227
|
// the interrupt, then the turn it opens; nothing to dispatch here.
|
|
9170
9228
|
case "forceSendTaskMessage":
|
|
9171
9229
|
await api.forceSendTaskMessage(effect.sessionId, effect.taskId, effect.text);
|
|
9172
9230
|
return;
|
|
9173
|
-
case "forceSendQueuedTaskMessage":
|
|
9174
|
-
|
|
9175
|
-
if (!sent) dispatch({ type: "notice", message: "That message has already reached the runner." });
|
|
9231
|
+
case "forceSendQueuedTaskMessage":
|
|
9232
|
+
await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
|
|
9176
9233
|
return;
|
|
9177
|
-
}
|
|
9178
9234
|
case "interruptTask":
|
|
9179
9235
|
await api.interruptTask(effect.sessionId, effect.taskId);
|
|
9180
|
-
dispatch({ type: "notice", message: "Interrupting the task\u2026" });
|
|
9181
9236
|
return;
|
|
9182
9237
|
case "setModel": {
|
|
9183
9238
|
const settled = await api.updateSettings({ orchestratorModel: effect.modelId });
|
|
@@ -9317,11 +9372,12 @@ async function perform(effect, deps) {
|
|
|
9317
9372
|
else unfollow(deps, effect.sessionId);
|
|
9318
9373
|
throw err;
|
|
9319
9374
|
}
|
|
9320
|
-
const { plan, goal } = adopted2;
|
|
9321
|
-
dispatch({ type: "
|
|
9375
|
+
const { plan, goal, run } = adopted2;
|
|
9376
|
+
dispatch({ type: "sessionLoaded", sessionId: effect.sessionId, goal });
|
|
9322
9377
|
dispatch(restoredChat(plan, effect.sessionId));
|
|
9323
9378
|
inbound.releaseSnapshot(true);
|
|
9324
9379
|
dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
|
|
9380
|
+
if (run) dispatch({ type: "runSnapshot", view: run, sessionId: effect.sessionId });
|
|
9325
9381
|
dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
|
|
9326
9382
|
return;
|
|
9327
9383
|
}
|
|
@@ -9361,6 +9417,12 @@ async function perform(effect, deps) {
|
|
|
9361
9417
|
// user gets to read — no separate notice to repeat it. The daemon also
|
|
9362
9418
|
// broadcast that summary; `redrawn` keeps a late socket copy of it from
|
|
9363
9419
|
// landing as a second, spoken turn.
|
|
9420
|
+
case "cancelPlannerResend": {
|
|
9421
|
+
const { cancelled } = await api.cancelPlannerResend(effect.sessionId);
|
|
9422
|
+
dispatch({ type: "notice", message: cancelled ? "Resend cancelled: what waits goes with your next message." : "Nothing waits on the planner's usage limit to be sent at a reset." });
|
|
9423
|
+
await refreshPlan(deps, effect.sessionId);
|
|
9424
|
+
return;
|
|
9425
|
+
}
|
|
9364
9426
|
case "compactConversation": {
|
|
9365
9427
|
const { plan } = await api.compactConversation(effect.sessionId);
|
|
9366
9428
|
const inbound = inboundFor(deps.api, deps.dispatch, effect.sessionId);
|
|
@@ -9376,16 +9438,15 @@ async function perform(effect, deps) {
|
|
|
9376
9438
|
// A conflict or a refusal is an answer, not a fault: either way the user's
|
|
9377
9439
|
// tree is exactly as it was, and the words say what to do next. A full
|
|
9378
9440
|
// merge of a settled run leaves nothing to hand over — the daemon has
|
|
9379
|
-
// cleared the run up
|
|
9441
|
+
// cleared the run up and says so on the stream (`run_closed`); one
|
|
9442
|
+
// mid-run leaves the run going (ADR-0020).
|
|
9380
9443
|
case "isolationMerge": {
|
|
9381
9444
|
const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
|
|
9382
|
-
if (ok && !effect.midRun) dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
9383
9445
|
dispatch({ type: ok ? "notice" : "failed", message });
|
|
9384
9446
|
return;
|
|
9385
9447
|
}
|
|
9386
9448
|
case "isolationDiscard":
|
|
9387
9449
|
await api.discardRun(effect.sessionId);
|
|
9388
|
-
dispatch({ type: "runCleared", sessionId: effect.sessionId });
|
|
9389
9450
|
dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
|
|
9390
9451
|
return;
|
|
9391
9452
|
case "isolationCleanup":
|
|
@@ -9423,6 +9484,12 @@ async function perform(effect, deps) {
|
|
|
9423
9484
|
} catch {
|
|
9424
9485
|
}
|
|
9425
9486
|
return;
|
|
9487
|
+
case "loadRunners":
|
|
9488
|
+
await loadRunners(deps);
|
|
9489
|
+
return;
|
|
9490
|
+
case "loadScope":
|
|
9491
|
+
await Promise.all([loadRunners(deps), loadSettings(deps)]);
|
|
9492
|
+
return;
|
|
9426
9493
|
case "refresh":
|
|
9427
9494
|
await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
|
|
9428
9495
|
if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
|
|
@@ -9527,8 +9594,7 @@ async function converse(deps, sessionId, call) {
|
|
|
9527
9594
|
await Promise.all([stream.ready, follow(deps, sessionId)]);
|
|
9528
9595
|
const plan = await call();
|
|
9529
9596
|
inbound.flush();
|
|
9530
|
-
const reply
|
|
9531
|
-
if (reply) inbound.backfill(reply.content, reply.timestamp);
|
|
9597
|
+
for (const reply of turnReplies(plan)) inbound.backfill(reply.content, reply.timestamp, reply.runs);
|
|
9532
9598
|
deps.dispatch({ type: "planUpdated", plan, sessionId });
|
|
9533
9599
|
} finally {
|
|
9534
9600
|
inbound.flush();
|
|
@@ -9546,17 +9612,18 @@ function restoredChat(plan, sessionId) {
|
|
|
9546
9612
|
sessionId
|
|
9547
9613
|
};
|
|
9548
9614
|
}
|
|
9549
|
-
function
|
|
9615
|
+
function turnReplies(plan) {
|
|
9550
9616
|
const history = plan?.conversationHistory ?? [];
|
|
9617
|
+
const replies = [];
|
|
9551
9618
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
9552
9619
|
const entry = history[i];
|
|
9553
|
-
if ((0, import_core50.isUserMessage)(entry))
|
|
9620
|
+
if ((0, import_core50.isUserMessage)(entry)) break;
|
|
9554
9621
|
if (entry.role === "assistant" && entry.kind === void 0) {
|
|
9555
|
-
const { content, timestamp } = entry;
|
|
9556
|
-
|
|
9622
|
+
const { content, timestamp, runs } = entry;
|
|
9623
|
+
replies.unshift({ content, ...timestamp ? { timestamp } : {}, ...runs ? { runs } : {} });
|
|
9557
9624
|
}
|
|
9558
9625
|
}
|
|
9559
|
-
return
|
|
9626
|
+
return replies;
|
|
9560
9627
|
}
|
|
9561
9628
|
function compactionSummary(plan) {
|
|
9562
9629
|
const history = plan?.conversationHistory ?? [];
|
|
@@ -9564,8 +9631,9 @@ function compactionSummary(plan) {
|
|
|
9564
9631
|
return entry ? { content: entry.content, ...entry.timestamp ? { timestamp: entry.timestamp } : {} } : null;
|
|
9565
9632
|
}
|
|
9566
9633
|
async function refreshPlan(deps, sessionId) {
|
|
9567
|
-
const { plan } = await deps.api.getSession(sessionId, deps.workspace);
|
|
9568
|
-
deps.dispatch({ type: "
|
|
9634
|
+
const { plan, run } = await deps.api.getSession(sessionId, deps.workspace);
|
|
9635
|
+
deps.dispatch({ type: "planRefreshed", plan, sessionId });
|
|
9636
|
+
if (run) deps.dispatch({ type: "runSnapshot", view: run, sessionId });
|
|
9569
9637
|
}
|
|
9570
9638
|
async function loadModels(deps) {
|
|
9571
9639
|
deps.dispatch({ type: "modelsLoaded", ...normalizeCatalog(await deps.api.getModels()) });
|
|
@@ -9986,10 +10054,12 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
|
|
|
9986
10054
|
if (terminal) app.dispatch({ type: "resize", ...terminal.size() });
|
|
9987
10055
|
}
|
|
9988
10056
|
});
|
|
10057
|
+
const watchingSettings = settings.watch(() => app.dispatch({ type: "sharedSettingsChanged" }));
|
|
9989
10058
|
const { rows, cols } = terminal.size();
|
|
9990
10059
|
app.dispatch({ type: "resize", rows, cols });
|
|
9991
10060
|
app.start();
|
|
9992
10061
|
function shutdown(code) {
|
|
10062
|
+
watchingSettings.dispose();
|
|
9993
10063
|
terminal?.close();
|
|
9994
10064
|
if (owned) stopDaemon(port).catch(() => {
|
|
9995
10065
|
});
|
|
@@ -10059,6 +10129,7 @@ var COMMANDS = {
|
|
|
10059
10129
|
fork: handleFork,
|
|
10060
10130
|
compact: handleCompact,
|
|
10061
10131
|
rewind: handleRewind,
|
|
10132
|
+
resend: handleResend,
|
|
10062
10133
|
handoff: handleHandoff,
|
|
10063
10134
|
tui: handleTui
|
|
10064
10135
|
};
|