@ordewell/cli 0.10.3 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +181 -77
  2. 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> Start a task now, ignoring dependencies (asks past a merge gate)
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
  };
@@ -644,6 +644,10 @@ var ApiClient = class _ApiClient {
644
644
  cancelResume(sessionId, taskId) {
645
645
  return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/cancel`, "Cancel resume failed");
646
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");
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
653
  return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/now`, "Resume failed");
@@ -1610,10 +1614,21 @@ var STATE_OF = {
1610
1614
  function reposOf(run) {
1611
1615
  if (Array.isArray(run.repos)) {
1612
1616
  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) => ({ path: String(r.path), integrationBranch: String(r.integrationBranch), baseRef: String(r.baseRef) })) : null;
1617
+ return repos.length > 0 ? repos.map((r) => ({
1618
+ path: String(r.path),
1619
+ integrationBranch: String(r.integrationBranch),
1620
+ baseRef: String(r.baseRef),
1621
+ ...typeof r.baseBranch === "string" && r.baseBranch ? { baseBranch: r.baseBranch } : {},
1622
+ ...r.startedDetached === true ? { startedDetached: true } : {}
1623
+ })) : null;
1614
1624
  }
1615
1625
  if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
1616
- return [{ path: ".", integrationBranch: run.integrationBranch, baseRef: run.baseRef }];
1626
+ return [{
1627
+ path: ".",
1628
+ integrationBranch: run.integrationBranch,
1629
+ baseRef: run.baseRef,
1630
+ ...typeof run.baseBranch === "string" && run.baseBranch ? { baseBranch: run.baseBranch } : {}
1631
+ }];
1617
1632
  }
1618
1633
  return null;
1619
1634
  }
@@ -1664,15 +1679,11 @@ function handoffBranch(handoff) {
1664
1679
  return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
1665
1680
  }
1666
1681
  function handoffBase(handoff, length) {
1667
- const group = isRepoGroup(handoff);
1668
- return handoff.repos.map((r) => `${group ? `${r.path} ` : ""}${r.baseRef.slice(0, length)}`).join(", ");
1669
- }
1670
- function isRepoGroup(handoff) {
1671
- return handoff.repos.some((r) => r.path !== ".");
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);
1682
+ const group = (0, import_core10.isRepoGroup)(handoff);
1683
+ return handoff.repos.map((r) => {
1684
+ const ref = r.baseRef.slice(0, length);
1685
+ return `${group ? `${r.path}: ` : ""}${r.baseBranch ? `${r.baseBranch} @ ${ref}` : `${r.startedDetached ? "detached at" : "base"} ${ref}`}`;
1686
+ }).join(", ");
1676
1687
  }
1677
1688
  function repairedLanded(handoff) {
1678
1689
  return handoff.landed.filter((t) => t.repairedFiles?.length);
@@ -1750,7 +1761,7 @@ async function followExecution(api, sessionId, start, onBlocked) {
1750
1761
  for (const t of entries) {
1751
1762
  const icon = iconFor(t.status);
1752
1763
  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 run-task <id>\` starts it` : "";
1764
+ const waits = t.waitsForApproval ? `: ${inLine(import_core12.WAITS_FOR_APPROVAL_LABEL)}: \`ordewell force-start <id>\` approves it, \`ordewell run\` approves all` : "";
1754
1765
  lines.push(` ${icon} #${t.id.slice(-4)} ${t.status}${verdict}${waits}`);
1755
1766
  }
1756
1767
  const output = lines.join("\n");
@@ -1812,10 +1823,10 @@ Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summ
1812
1823
  }
1813
1824
  if (event.type === "isolation_handoff") {
1814
1825
  const n = event.landed.length;
1815
- const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1826
+ const where = (0, import_core10.isRepoGroup)(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1816
1827
  console.log(`
1817
1828
  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}`);
1829
+ if ((0, import_core10.isRepoGroup)(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
1819
1830
  if (event.deferredTo === "supervisor") console.log(" The supervisor may merge it.");
1820
1831
  console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
1821
1832
  }
@@ -2656,27 +2667,28 @@ function formatCost(currency, amount) {
2656
2667
  function tokenLine(blocks2, cols) {
2657
2668
  const usage = blocks2[blocks2.length - 1];
2658
2669
  if (usage?.type !== "usage") return null;
2659
- const parts = usageMeasures(usage.totals, false);
2670
+ const parts = usageMeasures(usage.totals);
2660
2671
  if (usage.model && !usage.contextFill) parts.push("ctx unknown");
2661
2672
  if (usage.contextFill) parts.splice(2, 0, `${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}% ctx`);
2662
2673
  return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
2663
2674
  }
2664
- function usageMeasures(totals, unknown) {
2675
+ function usageMeasures(totals) {
2665
2676
  return [
2666
- ...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : unknown ? ["unknown in"] : [],
2667
- ...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : unknown ? ["unknown out"] : [],
2677
+ ...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : [],
2678
+ ...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : [],
2668
2679
  ...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"] : []
2680
+ ...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount))
2671
2681
  ];
2672
2682
  }
2673
2683
  function taskUsageText(usage) {
2674
2684
  return [
2675
- ...usageMeasures(usage.totals, true),
2676
- ...usage.attempt !== void 0 ? [usage.contextFill ? `${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}% ctx` : "ctx unknown"] : [],
2677
- usage.wallTimeMs !== void 0 ? `${Math.floor(usage.wallTimeMs / 1e3)}s` : "time unknown"
2685
+ ...usageMeasures(usage.totals),
2686
+ ...usage.attempt !== void 0 ? [usage.contextFill ? `ctx ${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}%` : "ctx unknown"] : []
2678
2687
  ].join(" \xB7 ");
2679
2688
  }
2689
+ function assignmentFlagText(row) {
2690
+ return `${row.assignmentFlag} \u2014 ${row.assignmentConsequence}`;
2691
+ }
2680
2692
 
2681
2693
  // src/commands/status.ts
2682
2694
  var import_fs4 = require("fs");
@@ -2694,7 +2706,7 @@ async function handleStatus(subArgs, injectedApi) {
2694
2706
  emitJson(session, outputPath);
2695
2707
  return;
2696
2708
  }
2697
- printOneSession(sessionId, session.meta, session.plan);
2709
+ printOneSession(sessionId, session.meta, session.plan, await assignmentScope(api));
2698
2710
  } else {
2699
2711
  const sessions = await api.getSessions(workspace);
2700
2712
  if (asJson) {
@@ -2733,7 +2745,20 @@ function emitJson(payload, outputPath) {
2733
2745
  process.exit(1);
2734
2746
  }
2735
2747
  }
2736
- function printOneSession(sessionId, meta, plan) {
2748
+ async function assignmentScope(api) {
2749
+ try {
2750
+ const [runners2, settings, models] = await Promise.all([api.getRunners(), api.getSettings(), api.getModels()]);
2751
+ if (!Array.isArray(runners2.runners) || runners2.runners.length === 0) return void 0;
2752
+ return {
2753
+ enabledRunners: runners2.runners.filter((r) => r.enabled).map((r) => r.id),
2754
+ allowlist: settings.modelAllowlist ?? {},
2755
+ models: models.modelsByRunner ?? {}
2756
+ };
2757
+ } catch {
2758
+ return void 0;
2759
+ }
2760
+ }
2761
+ function printOneSession(sessionId, meta, plan, runnerScope) {
2737
2762
  const allTasks = allTasksOf(plan);
2738
2763
  const everyTask = (0, import_core15.flattenTasks)(allTasks);
2739
2764
  const blocked = (0, import_core15.blockingFailures)(everyTask.map((t) => ({ ...t, subtasks: [], dependencies: t.dependencies ?? [] })));
@@ -2742,20 +2767,23 @@ function printOneSession(sessionId, meta, plan) {
2742
2767
  console.log(`Goal: ${meta.goal}`);
2743
2768
  console.log(`Runner: ${(meta.runners || []).join(", ")} | Status: ${meta.status}`);
2744
2769
  console.log(`Phase: ${plan?.phase || "unknown"} | Created: ${meta.createdAt}`);
2770
+ const isolation = isolationOfPlan(plan);
2771
+ if (isolation) console.log(`Base: ${handoffBase(isolation.handoff, 8)}`);
2745
2772
  console.log(`Tasks: ${everyTask.length} total | ${count("completed")} done | ${count("failed")} failed | ${count("in_progress")} running | ${count("blocked")} blocked | ${count("skipped")} skipped
2746
2773
  `);
2747
2774
  const usage = (0, import_core15.planUsage)(allTasks);
2748
2775
  if (usage.attempts) console.log(`Tasks total: ${taskUsageText(usage)}`);
2749
2776
  const record = servedApprovalRecord(plan);
2750
- const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell run-task ${ref}\` starts it, or \`ordewell run\` runs the plan` : null;
2777
+ const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell force-start ${ref}\` approves it, \`ordewell run\` approves all` : null;
2751
2778
  for (const t of allTasks) {
2752
2779
  const icon = iconFor(blocked.has(t.id) ? "blocked" : t.status);
2753
2780
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
2754
2781
  const shortId = (t.id ?? "").slice(-8);
2755
2782
  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 });
2783
+ const row = (0, import_core15.taskRowView)({ ...t, dependencies: t.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(t, record) }, { tasks: allTasks, runnerScope });
2757
2784
  const waiting = approval(row, (0, import_core15.taskOrderLabel)(t));
2758
2785
  if (waiting) console.log(` ${waiting}`);
2786
+ if (row.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(row)}`);
2759
2787
  if (row.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(row.blockedBy)}`);
2760
2788
  if (row.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(row.skipReason)}`);
2761
2789
  if (row.usage) console.log(` ${taskUsageText(row.usage)}`);
@@ -2764,9 +2792,10 @@ function printOneSession(sessionId, meta, plan) {
2764
2792
  const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
2765
2793
  const subShortId = (sub.id ?? "").slice(-8);
2766
2794
  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 });
2795
+ const subRow = (0, import_core15.taskRowView)({ ...sub, dependencies: sub.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(sub, record) }, { parent: t, tasks: allTasks, runnerScope });
2768
2796
  const subWaiting = approval(subRow, (0, import_core15.taskOrderLabel)(sub, t));
2769
2797
  if (subWaiting) console.log(` ${subWaiting}`);
2798
+ if (subRow.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(subRow)}`);
2770
2799
  if (subRow.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(subRow.blockedBy)}`);
2771
2800
  if (subRow.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(subRow.skipReason)}`);
2772
2801
  if (subRow.usage) console.log(` ${taskUsageText(subRow.usage)}`);
@@ -3263,8 +3292,8 @@ function makeHandler(action, command = action) {
3263
3292
  async (api, sessionId, taskId) => {
3264
3293
  if (PASSES_GATE.has(action)) await confirmPastGate(subArgs, api, sessionId, taskId, confirm2);
3265
3294
  try {
3266
- await api.taskControl(sessionId, taskId, action);
3267
- console.log(PAST_TENSE[action]);
3295
+ const { queued } = await api.taskControl(sessionId, taskId, action);
3296
+ console.log(queued ?? PAST_TENSE[action]);
3268
3297
  } catch (err) {
3269
3298
  console.error(`Failed to ${action} task: ${err.message}`);
3270
3299
  process.exit(1);
@@ -3561,6 +3590,17 @@ async function handleRewind(subArgs, injectedApi) {
3561
3590
  console.log("\nThe message you rewound to, to resend or edit:\n");
3562
3591
  console.log(fork.rewoundMessage);
3563
3592
  }
3593
+ async function handleResend(subArgs, injectedApi) {
3594
+ if (positionals(subArgs)[0] !== "cancel") fail("Usage: ordewell resend cancel [--session-id <id>] [--workspace /path]");
3595
+ const { api, sessionId } = await adopted(subArgs, injectedApi);
3596
+ let cancelled;
3597
+ try {
3598
+ ({ cancelled } = await api.cancelPlannerResend(sessionId));
3599
+ } catch (err) {
3600
+ fail(`Failed to cancel the resend: ${err.message}`);
3601
+ }
3602
+ 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.");
3603
+ }
3564
3604
 
3565
3605
  // src/tui/transcript.ts
3566
3606
  var import_core22 = require("@ordewell/core");
@@ -3594,7 +3634,7 @@ var HANDOFF_ACTIONS = [
3594
3634
  { id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
3595
3635
  ];
3596
3636
  function handoffActions(handoff) {
3597
- if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
3637
+ if (!(0, import_core10.isRepoGroup)(handoff)) return HANDOFF_ACTIONS;
3598
3638
  return [
3599
3639
  { id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
3600
3640
  { id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there (asks first)" },
@@ -3619,7 +3659,7 @@ function handoffCommand(state, arg) {
3619
3659
  }
3620
3660
  function handoffArrived(state, handoff) {
3621
3661
  const landed = handoff.landed.length;
3622
- const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
3662
+ const where = (0, import_core10.isRepoGroup)(handoff) ? ` in ${reposOf2(handoff)}` : "";
3623
3663
  const branch = handoffBranch(handoff);
3624
3664
  const tasks = `${landed} task${landed === 1 ? "" : "s"} landed`;
3625
3665
  const received = { ...state, handoff };
@@ -3654,8 +3694,8 @@ function runHandoffAction(state, id) {
3654
3694
  ...state,
3655
3695
  overlay: {
3656
3696
  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 whatever ${isRepoGroup(handoff) ? `each of ${reposWithWork(handoff).join(", ")} has` : "you have"} checked out. 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 ${isRepoGroup(handoff) ? "trees stay" : "tree stays"} as ${isRepoGroup(handoff) ? "they were" : "it was"}.${repairedNotice(handoff)}`,
3697
+ title: (0, import_core10.isRepoGroup)(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
3698
+ 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
3699
  action: { kind: "merge-run" }
3660
3700
  }
3661
3701
  },
@@ -3665,15 +3705,15 @@ function runHandoffAction(state, id) {
3665
3705
  return {
3666
3706
  state: {
3667
3707
  ...state,
3668
- overlay: isRepoGroup(handoff) ? {
3708
+ overlay: (0, import_core10.isRepoGroup)(handoff) ? {
3669
3709
  kind: "confirm",
3670
3710
  title: "Merge all into your branches?",
3671
- message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. 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)}`,
3711
+ 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
3712
  action: { kind: "merge-run" }
3673
3713
  } : {
3674
3714
  kind: "confirm",
3675
3715
  title: "Merge into your branch?",
3676
- message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. 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)}`,
3716
+ 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
3717
  action: { kind: "merge-run" }
3678
3718
  }
3679
3719
  },
@@ -3686,7 +3726,7 @@ function runHandoffAction(state, id) {
3686
3726
  overlay: {
3687
3727
  kind: "confirm",
3688
3728
  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.`,
3729
+ 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
3730
  action: { kind: "discard-run" }
3691
3731
  }
3692
3732
  },
@@ -3704,7 +3744,7 @@ function confirmedHandoff(state, kind) {
3704
3744
  type: "isolationMerge",
3705
3745
  sessionId,
3706
3746
  branch: handoffBranch(handoff),
3707
- ...isRepoGroup(handoff) ? { group: true } : {},
3747
+ ...(0, import_core10.isRepoGroup)(handoff) ? { group: true } : {},
3708
3748
  ...repaired.length > 0 ? { repaired } : {},
3709
3749
  ...midRun(state) ? { midRun: true } : {}
3710
3750
  } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
@@ -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 the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it; once a finished run has merged everything, its worktrees and branches are removed.` : `Merge ${branch} into the branch you have checked out? Once a finished run has merged everything, its worktrees and branches are removed.`) + repairedNotice(handoff));
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);
@@ -4426,6 +4466,7 @@ function initialState(overrides = {}) {
4426
4466
  workspace: process.cwd(),
4427
4467
  handoff: null,
4428
4468
  runMerge: null,
4469
+ plannerResend: null,
4429
4470
  overlay: null,
4430
4471
  pendingApprovals: [],
4431
4472
  queuedPrompts: import_core28.EMPTY_HOLD,
@@ -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: "Start a task now, ignoring dependencies", category: "tasks" },
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" },
@@ -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
  }
@@ -4993,15 +5044,17 @@ function plannerItems(state) {
4993
5044
  const runner = (0, import_core31.runnerForProvider)(id);
4994
5045
  usability[runner] = installed.has(runner) ? { usable: true } : { usable: false, reason: "CLI not installed or not on PATH" };
4995
5046
  }
4996
- const entries = (0, import_core31.plannerBackendEntries)(usability, state.configuredProviders);
4997
- const items = entries.map((entry) => ({
5047
+ const offered2 = (0, import_core31.plannerBackendEntries)(usability, state.configuredProviders).filter((entry) => entry.usable);
5048
+ if (offered2.length === 0) {
5049
+ return [{ id: "", label: "No planner available", detail: "install a coding agent or add a key with /key", disabled: true }];
5050
+ }
5051
+ const items = offered2.map((entry) => ({
4998
5052
  id: entry.id,
4999
5053
  label: entry.label,
5000
5054
  detail: entry.reason,
5001
- selected: state.plannerProvider === entry.id,
5002
- disabled: !entry.usable
5055
+ selected: state.plannerProvider === entry.id
5003
5056
  }));
5004
- return entries.some((entry) => entry.kind === "vendor") ? items : [...items, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5057
+ return offered2.some((entry) => entry.kind === "vendor") ? items : [...items, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5005
5058
  }
5006
5059
 
5007
5060
  // src/version.ts
@@ -5037,7 +5090,13 @@ var PLAN_KEYS = {
5037
5090
  deps: { keys: "D", meaning: "pick the tasks it depends on", group: "Assign" },
5038
5091
  skills: { keys: "K", meaning: "pick the skills it loads", group: "Assign" },
5039
5092
  ops: { keys: "O", meaning: "switch between a change and an ops task", group: "Assign" },
5040
- start: { keys: "f", meaning: "start the task now", group: "Run", hint: "f 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
+ },
5041
5100
  runPlan: { keys: "E", meaning: "run the whole plan", group: "Run", hint: "E run plan" },
5042
5101
  stop: { keys: "S", meaning: "stop the run", group: "Run", hint: "S stop" },
5043
5102
  cancel: {
@@ -5293,11 +5352,18 @@ function chatLayout(state, rows, cols) {
5293
5352
  const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
5294
5353
  const content = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
5295
5354
  const taskHeader = tv ? taskHeaderLines(state, tv, cols) : [];
5296
- 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)];
5297
5356
  const footer = usage.slice(0, Math.max(1, rows - taskHeader.length - 1));
5298
5357
  const space = Math.max(0, rows - taskHeader.length - footer.length);
5299
5358
  return { lines: content, header: taskHeader, footer, anchor: content.length > space ? "bottom" : "top", maxScroll: Math.max(0, content.length - space) };
5300
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
+ }
5301
5367
  var tokenRow = (blocks2, cols) => {
5302
5368
  const line = tokenLine(blocks2, cols);
5303
5369
  return line ? [line] : [];
@@ -5305,15 +5371,13 @@ var tokenRow = (blocks2, cols) => {
5305
5371
  function taskUsageFooter(state, tv, blocks2, cols) {
5306
5372
  const task = findTask(state.tasks, tv.taskId);
5307
5373
  const reported = task?.attemptUsage ?? [];
5374
+ if (!task || reported.length === 0) return tokenRow(blocks2, cols);
5308
5375
  const onScreen = tv.attempt > 0 ? reported.find((a) => a.attempt === tv.attempt) : reported.at(-1);
5309
- if (!task || !onScreen) return tokenRow(blocks2, cols);
5310
- const usage = (0, import_core34.taskRowView)({ ...task, attemptUsage: [onScreen] }).usage;
5311
- if (!usage) return tokenRow(blocks2, cols);
5312
- const models = usage.byModel ?? [];
5313
- return [
5314
- style.grey(truncate(taskUsageText(usage), cols)),
5315
- ...models.length > 1 ? models.map((m) => style.grey(truncate(`${sanitize(m.model ?? "unattributed")}: ${usageMeasures(m.totals, true).join(" \xB7 ")}`, cols))) : []
5316
- ];
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))];
5317
5381
  }
5318
5382
  function chatScrollMax(state) {
5319
5383
  return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
@@ -5556,9 +5620,17 @@ function planScrollExtent(state) {
5556
5620
  }
5557
5621
  return planLayout(state, bodyRows(state), cols);
5558
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
+ }
5559
5630
  function taskLines(state, planRow, index, cols) {
5560
5631
  const { task, parent } = planRow;
5561
- const row = (0, import_core34.taskRowView)(task, { parent, tasks: state.tasks, modes: modesForTask(state.modesByRunner, task) });
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 });
5562
5634
  const selected = state.focus === "plan" && index === state.selectedTask;
5563
5635
  const expanded = state.expandedTaskId === task.id;
5564
5636
  const rawIcon = row.status === "running" ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[row.status];
@@ -5580,8 +5652,7 @@ function taskLines(state, planRow, index, cols) {
5580
5652
  const prefix = lineIndex === 0 ? head : continuation;
5581
5653
  return prefix + (selected ? style.bold(line) : line);
5582
5654
  });
5583
- const reported = [...new Set((row.usage?.byModel ?? []).flatMap((m) => m.model ? [sanitize(m.model)] : []))];
5584
- 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" : "");
5585
5656
  const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
5586
5657
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
5587
5658
  const runner = task.assignedRunner ?? "";
@@ -5597,36 +5668,39 @@ function taskLines(state, planRow, index, cols) {
5597
5668
  if (row.conflict) {
5598
5669
  const where = row.conflict.repo ? ` in ${row.conflict.repo}` : "";
5599
5670
  const files = row.conflict.files ? ` (${row.conflict.files})` : "";
5600
- lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files}: its work is kept on its own branch`, cols)));
5671
+ lines.push(...statusLines(style.red, "\u26A0", `merge conflict${where}${files}: its work is kept on its own branch`, cols, bodyPad));
5601
5672
  }
5602
5673
  if (row.repairing) {
5603
5674
  const files = row.repairing.files ? ` in ${row.repairing.files}` : "";
5604
5675
  const attempt = row.repairing.attempt ? ` (attempt ${row.repairing.attempt.attempt}/${row.repairing.attempt.limit})` : "";
5605
- lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
5676
+ lines.push(...statusLines(style.yellow, "\u21BB", `repairing conflict${files}${attempt}`, cols, bodyPad));
5606
5677
  }
5607
5678
  if (row.mergeGate) {
5608
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all: ${row.mergeGate.join(", ")} not merged into your branch yet`, cols)));
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));
5609
5683
  }
5610
5684
  if (row.waitsForApproval) {
5611
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 ${inRow(row.waitsForApproval)}: added after you approved the plan; f starts it, or /run runs the plan`, cols)));
5685
+ lines.push(...statusLines(style.yellow, "\u23F8", `${inRow(row.waitsForApproval)} \xB7 f approves it \xB7 /run approves all`, cols, bodyPad));
5612
5686
  }
5613
5687
  if (row.blockedBy) {
5614
- lines.push(style.red(truncate(`${bodyPad}\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)));
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));
5615
5689
  }
5616
5690
  if (row.status === "skipped") {
5617
- lines.push(style.grey(truncate(`${bodyPad}\xBB ${inRow((0, import_core34.skippedLabel)(row.skipReason))}: nothing of it landed; m done lands its kept work`, cols)));
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));
5618
5692
  }
5619
5693
  const resumable = row.actions.includes("resume-now");
5620
5694
  if (row.resumes) {
5621
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 ${inRow(row.resumes)}, once the usage limit resets: ${resumable ? "r resume now \xB7 " : ""}c don't resume`, cols)));
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));
5622
5696
  } else if (resumable) {
5623
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for the usage limit to reset: r resume now`, cols)));
5697
+ lines.push(...statusLines(style.yellow, "\u23F8", "waits for the usage limit to reset: r resume now", cols, bodyPad));
5624
5698
  }
5625
5699
  if (row.opsChangedFiles) {
5626
- lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files: check them, then m done or /retry`, cols)));
5700
+ lines.push(...statusLines(style.yellow, "\u26A0", "an ops task changed tracked files: check them, then m done or /retry", cols, bodyPad));
5627
5701
  }
5628
5702
  if (row.repaired?.landed) {
5629
- lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${row.repaired.files}`, cols)));
5703
+ lines.push(...statusLines(style.grey, "\u21BB", `landed after repairing conflict in ${row.repaired.files}`, cols, bodyPad));
5630
5704
  }
5631
5705
  let editorLine;
5632
5706
  if (expanded) {
@@ -5668,6 +5742,11 @@ function taskLines(state, planRow, index, cols) {
5668
5742
  }
5669
5743
  return { lines, editorLine };
5670
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
+ }
5671
5750
  function taskText(label, text, cols, pad2 = " ") {
5672
5751
  const room = Math.max(1, cols - width(pad2));
5673
5752
  return [
@@ -6055,7 +6134,7 @@ function renderHandoff(state, overlay, rows, cols) {
6055
6134
  const caret = active ? style.brand("\u276F") : " ";
6056
6135
  return `${caret} ${active ? style.bold(action.label) : action.label}${style.grey(`: ${action.hint}`)}`;
6057
6136
  });
6058
- 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]}`), ""] : [];
6059
6138
  return frame(
6060
6139
  "Run handoff",
6061
6140
  [
@@ -7591,6 +7670,8 @@ function runCommand(state, { name, args }) {
7591
7670
  return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
7592
7671
  case "rewind":
7593
7672
  return rewind(state, args[0]);
7673
+ case "resend":
7674
+ 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."));
7594
7675
  case "compact":
7595
7676
  return withIdlePlanner(
7596
7677
  state,
@@ -7864,7 +7945,7 @@ function runners(state, args) {
7864
7945
  multi: true,
7865
7946
  chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
7866
7947
  })
7867
- } });
7948
+ } }, [{ type: "loadRunners" }]);
7868
7949
  }
7869
7950
  function setMaxParallel(state, arg) {
7870
7951
  if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once: /parallel <n> changes it.`));
@@ -8358,13 +8439,15 @@ function reduceAction(state, action) {
8358
8439
  focus: "chat",
8359
8440
  editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
8360
8441
  });
8442
+ case "sharedSettingsChanged":
8443
+ return step(state, [{ type: "loadScope" }]);
8361
8444
  case "runnersLoaded":
8362
- return step(refreshStepper(refillPicker({
8445
+ return step(refreshStepper(reseedRunnerChoice(state, refillPicker({
8363
8446
  ...state,
8364
8447
  runners: action.runners,
8365
8448
  runnersKnown: true,
8366
8449
  orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
8367
- }, ["set-task-runner"])));
8450
+ }, ["set-task-runner"]))));
8368
8451
  case "failed": {
8369
8452
  if (!action.turn) return step(say(taskLogAbandoned(state), "error", action.message));
8370
8453
  if (state.forceSendPending) {
@@ -8411,6 +8494,7 @@ function showPlan(state, plan) {
8411
8494
  const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
8412
8495
  const next = {
8413
8496
  ...folded,
8497
+ plannerResend: plannerResendOf(plan),
8414
8498
  expandedTaskId,
8415
8499
  taskEditor: expandedTaskId !== null ? state.taskEditor : null
8416
8500
  };
@@ -8420,6 +8504,11 @@ function showPlan(state, plan) {
8420
8504
  focus: tasks.length === 0 ? "chat" : state.focus
8421
8505
  });
8422
8506
  }
8507
+ function plannerResendOf(plan) {
8508
+ const saved = plan?.plannerResend;
8509
+ if (typeof saved?.at !== "string") return null;
8510
+ return { ...typeof saved.runner === "string" ? { runner: saved.runner } : {}, at: saved.at };
8511
+ }
8423
8512
  function runStatus(state, tasks) {
8424
8513
  if (tasks.some(import_core28.isTaskRunning)) return "executing";
8425
8514
  if (tasks.some((t) => t.status === "awaiting_user") || state.runMerge?.paused) return "executing";
@@ -9359,6 +9448,12 @@ async function perform(effect, deps) {
9359
9448
  // user gets to read — no separate notice to repeat it. The daemon also
9360
9449
  // broadcast that summary; `redrawn` keeps a late socket copy of it from
9361
9450
  // landing as a second, spoken turn.
9451
+ case "cancelPlannerResend": {
9452
+ const { cancelled } = await api.cancelPlannerResend(effect.sessionId);
9453
+ 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." });
9454
+ await refreshPlan(deps, effect.sessionId);
9455
+ return;
9456
+ }
9362
9457
  case "compactConversation": {
9363
9458
  const { plan } = await api.compactConversation(effect.sessionId);
9364
9459
  const inbound = inboundFor(deps.api, deps.dispatch, effect.sessionId);
@@ -9421,6 +9516,12 @@ async function perform(effect, deps) {
9421
9516
  } catch {
9422
9517
  }
9423
9518
  return;
9519
+ case "loadRunners":
9520
+ await loadRunners(deps);
9521
+ return;
9522
+ case "loadScope":
9523
+ await Promise.all([loadRunners(deps), loadSettings(deps)]);
9524
+ return;
9424
9525
  case "refresh":
9425
9526
  await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
9426
9527
  if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
@@ -9984,10 +10085,12 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
9984
10085
  if (terminal) app.dispatch({ type: "resize", ...terminal.size() });
9985
10086
  }
9986
10087
  });
10088
+ const watchingSettings = settings.watch(() => app.dispatch({ type: "sharedSettingsChanged" }));
9987
10089
  const { rows, cols } = terminal.size();
9988
10090
  app.dispatch({ type: "resize", rows, cols });
9989
10091
  app.start();
9990
10092
  function shutdown(code) {
10093
+ watchingSettings.dispose();
9991
10094
  terminal?.close();
9992
10095
  if (owned) stopDaemon(port).catch(() => {
9993
10096
  });
@@ -10057,6 +10160,7 @@ var COMMANDS = {
10057
10160
  fork: handleFork,
10058
10161
  compact: handleCompact,
10059
10162
  rewind: handleRewind,
10163
+ resend: handleResend,
10060
10164
  handoff: handleHandoff,
10061
10165
  tui: handleTui
10062
10166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/cli",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",
@@ -51,8 +51,8 @@
51
51
  "typecheck": "tsc --noEmit"
52
52
  },
53
53
  "dependencies": {
54
- "@ordewell/core": "^0.10.3",
55
- "@ordewell/web": "^0.10.3",
54
+ "@ordewell/core": "^0.10.5",
55
+ "@ordewell/web": "^0.10.5",
56
56
  "string-width": "^7.2.0",
57
57
  "ws": "^8.18.0"
58
58
  },