muonroi-cli 1.6.4 → 1.6.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.
Files changed (37) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -2
  3. package/dist/packages/agent-harness-core/src/protocol.d.ts +8 -0
  4. package/dist/src/generated/version.d.ts +1 -1
  5. package/dist/src/generated/version.js +1 -1
  6. package/dist/src/gsd/__tests__/directives.test.js +37 -0
  7. package/dist/src/gsd/directives.d.ts +18 -0
  8. package/dist/src/gsd/directives.js +23 -2
  9. package/dist/src/orchestrator/message-processor.d.ts +8 -0
  10. package/dist/src/orchestrator/message-processor.js +56 -7
  11. package/dist/src/orchestrator/orchestrator.d.ts +10 -0
  12. package/dist/src/orchestrator/orchestrator.js +11 -0
  13. package/dist/src/orchestrator/stall-rescue.d.ts +1 -0
  14. package/dist/src/orchestrator/stall-rescue.js +20 -1
  15. package/dist/src/orchestrator/stall-rescue.test.js +30 -1
  16. package/dist/src/orchestrator/steer-inbox.d.ts +32 -0
  17. package/dist/src/orchestrator/steer-inbox.js +20 -0
  18. package/dist/src/orchestrator/steer-inbox.test.d.ts +1 -0
  19. package/dist/src/orchestrator/steer-inbox.test.js +33 -0
  20. package/dist/src/orchestrator/tool-loop-askcard.d.ts +59 -0
  21. package/dist/src/orchestrator/tool-loop-askcard.js +86 -0
  22. package/dist/src/orchestrator/tool-loop-askcard.test.d.ts +1 -0
  23. package/dist/src/orchestrator/tool-loop-askcard.test.js +71 -0
  24. package/dist/src/pil/layer4-gsd.js +5 -1
  25. package/dist/src/ui/app.js +142 -59
  26. package/dist/src/ui/hooks/use-session-picker.d.ts +14 -0
  27. package/dist/src/ui/hooks/use-session-picker.js +20 -0
  28. package/dist/src/ui/modals/session-picker-modal.d.ts +14 -0
  29. package/dist/src/ui/modals/session-picker-modal.js +39 -0
  30. package/dist/src/ui/utils/relaunch.d.ts +41 -0
  31. package/dist/src/ui/utils/relaunch.js +71 -0
  32. package/dist/src/ui/utils/relaunch.test.d.ts +1 -0
  33. package/dist/src/ui/utils/relaunch.test.js +83 -0
  34. package/dist/src/utils/settings.d.ts +10 -0
  35. package/dist/src/utils/settings.js +12 -0
  36. package/dist/src/utils/settings.test.js +21 -0
  37. package/package.json +1 -1
@@ -0,0 +1,86 @@
1
+ /**
2
+ * src/orchestrator/tool-loop-askcard.ts
3
+ *
4
+ * Pure helper that computes the tool-loop-cap askcard tier (label set + default
5
+ * action) from the current step number and the resolved natural ceiling for
6
+ * the (taskType, size) matrix.
7
+ *
8
+ * Four tiers (open intervals — boundaries belong to the higher tier):
9
+ * - early : step < 0.5 × ceiling — a transient fixation. Default Continue.
10
+ * - normal : 0.5× ≤ step ≤ 2× ceiling — used cheap budget; Default Stop.
11
+ * - overBudget : 2× < step ≤ 5× ceiling — Continue still available but the
12
+ * label carries the overage multiplier so the cost of
13
+ * continuing is visible at decision time. Default Stop.
14
+ * - extreme : step > 5× ceiling — Stop is moved FIRST in the option
15
+ * array (Enter = Stop) and Continue is labelled "expensive".
16
+ * Default Stop (now at index 0).
17
+ *
18
+ * Live miss this tier set fixes (session 1f29e238, step 77/6 = 12.8×): extreme
19
+ * tier put Stop first with a warning — good. But the storyflow_ui session
20
+ * 22661c8de9f2 ran step 29/12 = 2.4× — the OLD code had no middle warning, so
21
+ * the askcard showed a plain "Continue (let agent try)" with no signal that
22
+ * continuing costs more. User chose Continue, the model stalled 4 tool-calls
23
+ * later, and forced-finalize had to rescue a degraded answer.
24
+ *
25
+ * Pure — no React, no DOM, no side effects. Unit-testable in isolation.
26
+ */
27
+ const NORMAL_LABELS = ["Continue (let agent try)", "Stop and answer"];
28
+ const NORMAL_VALUES = ["continue", "stop"];
29
+ /**
30
+ * Decide the askcard layout for a tool-loop-cap pattern hit. Pure.
31
+ */
32
+ export function planLoopCapAskcard(opts) {
33
+ const { stepNumber, naturalCeiling } = opts;
34
+ // No ceiling → cannot compute multipliers. Fall back to a static threshold:
35
+ // step ≤ 15 looks "early" enough to default Continue, else default Stop.
36
+ if (!naturalCeiling || naturalCeiling <= 0) {
37
+ const tier = stepNumber > 0 && stepNumber <= 15 ? "early" : "normal";
38
+ return {
39
+ tier,
40
+ defaultIndex: tier === "early" ? 0 : 1,
41
+ optionLabels: NORMAL_LABELS,
42
+ optionValues: NORMAL_VALUES,
43
+ overageMultiplier: null,
44
+ };
45
+ }
46
+ const ratio = stepNumber / naturalCeiling;
47
+ const multiplier = ratio.toFixed(1);
48
+ if (ratio > 5) {
49
+ return {
50
+ tier: "extreme",
51
+ defaultIndex: 0,
52
+ optionLabels: ["Stop and answer (recommended)", `Continue anyway (⚠ ${multiplier}× over budget — expensive)`],
53
+ optionValues: ["stop", "continue"],
54
+ overageMultiplier: multiplier,
55
+ };
56
+ }
57
+ if (ratio > 2) {
58
+ return {
59
+ tier: "overBudget",
60
+ defaultIndex: 1,
61
+ optionLabels: [
62
+ `Continue (⚠ ${multiplier}× past natural budget — quality may degrade)`,
63
+ "Stop and answer (recommended)",
64
+ ],
65
+ optionValues: NORMAL_VALUES,
66
+ overageMultiplier: multiplier,
67
+ };
68
+ }
69
+ if (ratio < 0.5) {
70
+ return {
71
+ tier: "early",
72
+ defaultIndex: 0,
73
+ optionLabels: NORMAL_LABELS,
74
+ optionValues: NORMAL_VALUES,
75
+ overageMultiplier: null,
76
+ };
77
+ }
78
+ return {
79
+ tier: "normal",
80
+ defaultIndex: 1,
81
+ optionLabels: NORMAL_LABELS,
82
+ optionValues: NORMAL_VALUES,
83
+ overageMultiplier: null,
84
+ };
85
+ }
86
+ //# sourceMappingURL=tool-loop-askcard.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { planLoopCapAskcard } from "./tool-loop-askcard.js";
3
+ describe("planLoopCapAskcard", () => {
4
+ it("early tier (< 0.5× ceiling): default Continue, no warning", () => {
5
+ const r = planLoopCapAskcard({ stepNumber: 5, naturalCeiling: 12 });
6
+ expect(r.tier).toBe("early");
7
+ expect(r.defaultIndex).toBe(0);
8
+ expect(r.optionLabels[0]).toMatch(/Continue/);
9
+ expect(r.optionValues[0]).toBe("continue");
10
+ expect(r.overageMultiplier).toBeNull();
11
+ // no warning emoji on the Continue label
12
+ expect(r.optionLabels[0]).not.toMatch(/⚠/);
13
+ });
14
+ it("normal tier (0.5×–2× ceiling): default Stop, no warning, Continue first", () => {
15
+ const r = planLoopCapAskcard({ stepNumber: 18, naturalCeiling: 12 });
16
+ expect(r.tier).toBe("normal");
17
+ expect(r.defaultIndex).toBe(1);
18
+ expect(r.optionLabels[0]).toBe("Continue (let agent try)");
19
+ expect(r.optionLabels[1]).toBe("Stop and answer");
20
+ expect(r.overageMultiplier).toBeNull();
21
+ });
22
+ it("overBudget tier (2×–5× ceiling): Continue carries the overage multiplier, default Stop", () => {
23
+ // The storyflow_ui case: step 29 / ceiling 12 = 2.4×
24
+ const r = planLoopCapAskcard({ stepNumber: 29, naturalCeiling: 12 });
25
+ expect(r.tier).toBe("overBudget");
26
+ expect(r.defaultIndex).toBe(1);
27
+ expect(r.optionLabels[0]).toMatch(/⚠ 2\.4× past natural budget/);
28
+ expect(r.optionLabels[1]).toMatch(/Stop and answer \(recommended\)/);
29
+ expect(r.overageMultiplier).toBe("2.4");
30
+ // order preserved: Continue at 0, Stop at 1
31
+ expect(r.optionValues).toEqual(["continue", "stop"]);
32
+ });
33
+ it("extreme tier (> 5× ceiling): Stop FIRST in the array, Continue labelled expensive", () => {
34
+ // session 1f29e238 — step 77 / ceiling 6 = 12.8×
35
+ const r = planLoopCapAskcard({ stepNumber: 77, naturalCeiling: 6 });
36
+ expect(r.tier).toBe("extreme");
37
+ expect(r.defaultIndex).toBe(0);
38
+ expect(r.optionLabels[0]).toMatch(/Stop and answer \(recommended\)/);
39
+ expect(r.optionLabels[1]).toMatch(/⚠ 12\.8× over budget — expensive/);
40
+ expect(r.optionValues).toEqual(["stop", "continue"]); // ORDER REVERSED at extreme
41
+ expect(r.overageMultiplier).toBe("12.8");
42
+ });
43
+ it("tier boundaries are open-on-the-lower-side (ratio==2 → normal; ratio==5 → overBudget; ratio==0.5 → normal)", () => {
44
+ // ratio === 2.0 exactly → still normal (the > 2 gate excludes 2.0)
45
+ expect(planLoopCapAskcard({ stepNumber: 24, naturalCeiling: 12 }).tier).toBe("normal");
46
+ // ratio === 5.0 exactly → still overBudget (the > 5 gate excludes 5.0)
47
+ expect(planLoopCapAskcard({ stepNumber: 60, naturalCeiling: 12 }).tier).toBe("overBudget");
48
+ // ratio === 0.5 exactly → normal (the < 0.5 gate excludes 0.5)
49
+ expect(planLoopCapAskcard({ stepNumber: 6, naturalCeiling: 12 }).tier).toBe("normal");
50
+ });
51
+ it("falls back to step-threshold heuristic when naturalCeiling is missing", () => {
52
+ const early = planLoopCapAskcard({ stepNumber: 8 });
53
+ expect(early.tier).toBe("early");
54
+ expect(early.defaultIndex).toBe(0);
55
+ const normal = planLoopCapAskcard({ stepNumber: 22 });
56
+ expect(normal.tier).toBe("normal");
57
+ expect(normal.defaultIndex).toBe(1);
58
+ // boundary: step === 15 → still early
59
+ expect(planLoopCapAskcard({ stepNumber: 15 }).tier).toBe("early");
60
+ // step === 16 → normal
61
+ expect(planLoopCapAskcard({ stepNumber: 16 }).tier).toBe("normal");
62
+ // step === 0 → normal (no early credit for nothing)
63
+ expect(planLoopCapAskcard({ stepNumber: 0 }).tier).toBe("normal");
64
+ });
65
+ it("treats naturalCeiling=0 the same as undefined (no multiplier possible)", () => {
66
+ const r = planLoopCapAskcard({ stepNumber: 30, naturalCeiling: 0 });
67
+ expect(r.overageMultiplier).toBeNull();
68
+ expect(r.tier).toBe("normal");
69
+ });
70
+ });
71
+ //# sourceMappingURL=tool-loop-askcard.test.js.map
@@ -102,7 +102,11 @@ export async function layer4Gsd(ctx) {
102
102
  (ctx.taskType === "general" && ctx.intentKind === "task") ||
103
103
  (isQuestionLike(ctx.raw) && !isImplementationIntent(ctx.raw));
104
104
  const ecosystem = mentionsEcosystemScope(ctx.raw);
105
- const directive = buildDirective({ complexity, phase, grayAreas, informational, ecosystem });
105
+ // Heuristic: VN diacritics user wrote Vietnamese re-anchor language rule
106
+ // inside the directive (storyflow_ui session 22661c8de9f2 — base rule
107
+ // crowded out by brevity/FIX-FIRST directives).
108
+ const replyLanguage = /[à-ỹÀ-Ỹ]/.test(ctx.raw) ? "Vietnamese" : undefined;
109
+ const directive = buildDirective({ complexity, phase, grayAreas, informational, ecosystem, replyLanguage });
106
110
  const budgetChars = Math.floor(ctx.tokenBudget * DIRECTIVE_BUDGET_FRACTION);
107
111
  const trimmed = truncateToBudget(directive.text, budgetChars);
108
112
  return {
@@ -13,6 +13,7 @@ import { POPULAR_MCP_CATALOG } from "../mcp/catalog.js";
13
13
  import { parseEnvLines, parseHeaderLines } from "../mcp/parse-headers.js";
14
14
  import { toMcpServerId, validateMcpServerConfig } from "../mcp/validate.js";
15
15
  import { Agent } from "../orchestrator/orchestrator.js";
16
+ import { planLoopCapAskcard } from "../orchestrator/tool-loop-askcard.js";
16
17
  import { getConfiguredProviders, setKeyForProvider } from "../providers/keychain.js";
17
18
  import { buildIdealContinuationPrompt } from "../scaffold/continuation-prompt.js";
18
19
  import { continueAsCouncil } from "../scaffold/continue-as-council.js";
@@ -24,7 +25,7 @@ import { processAtMentions } from "../utils/at-mentions.js";
24
25
  import { readClipboardImage } from "../utils/clipboard-image.js";
25
26
  import { FileIndex } from "../utils/file-index.js";
26
27
  import { copyTextToHostClipboard, readTextFromHostClipboard } from "../utils/host-clipboard.js";
27
- import { getApiKey, getCurrentModel, getTelegramBotToken, isModelDisabled, isReservedSubagentName, loadMcpServers, loadPaymentSettings, loadUserSettings, loadValidSubAgents, saveApprovedTelegramUserId, saveMcpServers, savePaymentSettings, saveProjectSettings, saveUserSettings, setDefaultProvider, setModelDisabled, setProviderDisabled, } from "../utils/settings.js";
28
+ import { getApiKey, getCurrentModel, getSteerInjectionEnabled, getTelegramBotToken, isModelDisabled, isReservedSubagentName, loadMcpServers, loadPaymentSettings, loadUserSettings, loadValidSubAgents, saveApprovedTelegramUserId, saveMcpServers, savePaymentSettings, saveProjectSettings, saveUserSettings, setDefaultProvider, setModelDisabled, setProviderDisabled, } from "../utils/settings.js";
28
29
  import { discoverSkills, formatSkillsForChat } from "../utils/skills.js";
29
30
  import { formatSubagentName } from "../utils/subagent-display.js";
30
31
  import { checkForUpdate, runUpdate } from "../utils/update-checker.js";
@@ -57,6 +58,7 @@ import { usePairQuoteBuffer } from "./components/use-pair-quote-buffer.js";
57
58
  import { useAgentEditor } from "./hooks/use-agent-editor.js";
58
59
  import { useMcpEditor } from "./hooks/use-mcp-editor.js";
59
60
  import { useModelPicker } from "./hooks/use-model-picker.js";
61
+ import { useSessionPicker } from "./hooks/use-session-picker.js";
60
62
  import { useTypeahead } from "./hooks/useTypeahead.js";
61
63
  import { Markdown } from "./markdown.js";
62
64
  import { buildMcpBrowseRows, McpBrowserModal, McpEditorModal } from "./mcp-modal.js";
@@ -65,6 +67,7 @@ import { ApiKeyModal } from "./modals/api-key-modal.js";
65
67
  import { ConnectModal, TelegramPairModal, TelegramTokenModal } from "./modals/connect-modal.js";
66
68
  import { ModelPickerModal } from "./modals/model-picker-modal.js";
67
69
  import { SandboxPickerModal } from "./modals/sandbox-picker-modal.js";
70
+ import { SessionPickerModal } from "./modals/session-picker-modal.js";
68
71
  import { UpdateModal } from "./modals/update-modal.js";
69
72
  import { PaymentApprovalPanel, WalletPickerModal } from "./modals/wallet-picker-modal.js";
70
73
  import { resolvePickerProviders } from "./picker-providers.js";
@@ -76,6 +79,7 @@ import { StatusBar } from "./status-bar/index.js";
76
79
  import { statusBarStore, wireStatusBar } from "./status-bar/store.js";
77
80
  import { getCompactTuiSelectionText } from "./terminal-selection-text.js";
78
81
  import { dark } from "./theme.js";
82
+ import { relaunchWithSession } from "./utils/relaunch.js";
79
83
  import "./slash/route.js";
80
84
  import "./slash/optimize.js";
81
85
  import "./slash/discuss.js";
@@ -483,6 +487,11 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
483
487
  pushToast(lvl, text);
484
488
  return;
485
489
  }
490
+ if (e.kind === "steer-inject") {
491
+ const count = typeof e.count === "number" ? e.count : 1;
492
+ pushToast("info", `↳ steering applied (${count} message${count === 1 ? "" : "s"})`);
493
+ return;
494
+ }
486
495
  if (e.kind === "ee-timeout" || e.kind === "ee-error") {
487
496
  const source = typeof e.source === "string" ? e.source : "unknown";
488
497
  const kind = e.kind === "ee-timeout" ? "timeout" : "error";
@@ -539,9 +548,27 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
539
548
  }
540
549
  return undefined;
541
550
  }, [handleHarnessEvent]);
551
+ // Live-queue steering: expose the mid-turn queue to the running turn so
552
+ // prepareStep can inject typed-while-busy messages at the next step boundary
553
+ // instead of deferring them to a new turn. Disabled → callback not wired, so
554
+ // finishTurnProcessing drains the queue post-turn exactly as before.
555
+ useEffect(() => {
556
+ if (!getSteerInjectionEnabled())
557
+ return;
558
+ agent.setSteerDrain(() => {
559
+ if (queuedMessagesRef.current.length === 0)
560
+ return [];
561
+ const drained = queuedMessagesRef.current.map((m) => ({ text: m.text }));
562
+ queuedMessagesRef.current = [];
563
+ setQueuedMessages([]);
564
+ return drained;
565
+ });
566
+ return () => agent.setSteerDrain(null);
567
+ }, [agent]);
542
568
  const dismissToast = useCallback(() => setActiveToast(null), []);
543
569
  // ─── /Phase 21 toast subscriber ────────────────────────────────────────────
544
570
  const { model, setModel, showModelPicker, setShowModelPicker, modelPickerIndex, setModelPickerIndex, modelSearchQuery, setModelSearchQuery, configuredProviders, setConfiguredProviders, disabledProviders, setDisabledProvidersState, defaultProvider, setDefaultProviderState, disabledModels, setDisabledModelsState, modelPickerFocus, setModelPickerFocus, providerChipIndex, setProviderChipIndex, reasoningEffortByModel, setReasoningEffortByModel, } = useModelPicker(agent.getModel());
571
+ const { showSessionPicker, setShowSessionPicker, sessionPickerIndex, setSessionPickerIndex, sessions: sessionPickerList, setSessions: setSessionPickerList, } = useSessionPicker();
545
572
  const modelRef = useRef(model);
546
573
  const [providersWithKey, setProvidersWithKey] = useState(() => new Set());
547
574
  const refreshProvidersWithKey = useCallback(async () => {
@@ -1779,50 +1806,43 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
1779
1806
  const isPattern = info.kind === "pattern";
1780
1807
  const qid = isPattern ? `tool-pattern-loop-${Date.now()}` : `tool-loop-cap-${info.stepNumber}-${Date.now()}`;
1781
1808
  toolLoopCapResolversRef.current.set(qid, resolve);
1782
- // Phase 5 BUG-Hcontext-aware default:
1783
- // - Early in the run (step < natural ceiling × 0.5) loops are
1784
- // usually a temporary fixation on a single file/cmd; "continue"
1785
- // is the right default.
1786
- // - Past the soft-warn line (≥ 50% of natural ceiling) we've used
1787
- // up the cheap budget — "stop" becomes the safer default.
1788
- // Falls back to a static stepNumber heuristic (≤ 15) when caller
1789
- // didn't supply a naturalCeiling.
1809
+ // Tier-aware askcard layout (planLoopCapAskcard) 4 tiers:
1810
+ // early (< 0.5× ceiling) Default Continue, no warning
1811
+ // normal (0.5×–2× ceiling) → Default Stop, no warning
1812
+ // overBudget (2×–5× ceiling) → Default Stop, Continue label carries
1813
+ // the overage multiplier so cost is
1814
+ // visible (storyflow_ui 22661c8de9f2:
1815
+ // 2.4× hit had no warning before)
1816
+ // extreme (> ceiling) → Stop FIRST in the array (Enter=Stop),
1817
+ // Continue labelled "expensive"
1818
+ // (session 1f29e238: 12.8× past ceiling)
1790
1819
  const patternStep = isPattern ? info.stepNumber : 0;
1791
1820
  const patternCeiling = isPattern ? info.naturalCeiling : undefined;
1792
- const patternEarly = patternCeiling !== undefined
1793
- ? patternStep < Math.floor(patternCeiling * 0.5)
1794
- : patternStep > 0 && patternStep <= 15;
1795
- // Extreme-overage trip: stepNumber > 5× naturalCeiling. Evidence
1796
- // (session 1f29e238): at step 77/6 = 12.8× ceiling the askcard still
1797
- // showed Continue as a first-class option and user chose Continue
1798
- // within 4s. At extreme overage we put Stop FIRST (Enter = Stop) and
1799
- // label Continue with the explicit overage multiplier so the cost is
1800
- // visible at decision time.
1801
- const patternExtreme = patternCeiling !== undefined && patternCeiling > 0 && patternStep > patternCeiling * 5;
1802
- const overageMultiplier = patternExtreme && patternCeiling ? (patternStep / patternCeiling).toFixed(1) : null;
1803
- const patternDefaultIdx = patternEarly ? 0 : patternExtreme ? 0 : 1;
1804
- const patternOptions = patternExtreme
1821
+ const layout = isPattern
1822
+ ? planLoopCapAskcard({ stepNumber: patternStep, naturalCeiling: patternCeiling })
1823
+ : null;
1824
+ const patternEarly = layout?.tier === "early";
1825
+ const patternOverBudget = layout?.tier === "overBudget";
1826
+ const patternExtreme = layout?.tier === "extreme";
1827
+ const overageMultiplier = layout?.overageMultiplier ?? null;
1828
+ const patternDefaultIdx = layout?.defaultIndex ?? 0;
1829
+ const patternOptions = layout
1805
1830
  ? [
1806
- { label: "Stop and answer (recommended)", value: "stop", kind: "choice" },
1807
- {
1808
- label: `Continue anyway (⚠ ${overageMultiplier}× over budget — expensive)`,
1809
- value: "continue",
1810
- kind: "choice",
1811
- },
1831
+ { label: layout.optionLabels[0], value: layout.optionValues[0], kind: "choice" },
1832
+ { label: layout.optionLabels[1], value: layout.optionValues[1], kind: "choice" },
1812
1833
  ]
1813
- : [
1814
- { label: "Continue (let agent try)", value: "continue", kind: "choice" },
1815
- { label: "Stop and answer", value: "stop", kind: "choice" },
1816
- ];
1834
+ : [];
1817
1835
  const question = isPattern
1818
1836
  ? {
1819
1837
  questionId: qid,
1820
1838
  question: `Tool \`${info.toolName}\` đã chạy ${info.count}/${info.windowSize} lần với args gần giống (step ${info.stepNumber}${patternCeiling ? `/${patternCeiling}` : ""}) — có thể đang loop. Tiếp tục?`,
1821
1839
  context: patternExtreme
1822
1840
  ? `EXTREME OVERAGE — ${overageMultiplier}× past natural budget. Continuing has historically not converged in this regime (see session 1f29e238: 8× over budget, still failed). Stop returns the agent's best answer with current context.`
1823
- : patternEarly
1824
- ? "Continue lets the agent keep trying likely the right call this early in the run. Stop returns the agent's best answer with current context."
1825
- : "You're past the natural budget for this task type. Stop usually recovers a clean answer; Continue keeps spending tokens.",
1841
+ : patternOverBudget
1842
+ ? `Past natural budget — ${overageMultiplier}× the typical step count for this task type. Continuing may still converge but quality often degrades (longer compaction, stale tool results, forced-finalize on stall). Stop returns the agent's best answer with current context.`
1843
+ : patternEarly
1844
+ ? "Continue lets the agent keep trying — likely the right call this early in the run. Stop returns the agent's best answer with current context."
1845
+ : "You're past the natural budget for this task type. Stop usually recovers a clean answer; Continue keeps spending tokens.",
1826
1846
  isRequired: true,
1827
1847
  phase: "tool-loop-cap",
1828
1848
  options: patternOptions,
@@ -3035,6 +3055,27 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
3035
3055
  openSandboxPicker();
3036
3056
  return true;
3037
3057
  }
3058
+ if (c === "/sessions" || c === "/session") {
3059
+ try {
3060
+ const { SessionStore } = require("../storage/sessions.js");
3061
+ const list = new SessionStore(agent.getCwd()).listRecentSessions(20);
3062
+ setSessionPickerList(list);
3063
+ setSessionPickerIndex(0);
3064
+ setShowSessionPicker(true);
3065
+ }
3066
+ catch (err) {
3067
+ console.error(`[session-picker] list failed: ${err?.message ?? err}`);
3068
+ setMessages((p) => [
3069
+ ...p,
3070
+ {
3071
+ type: "assistant",
3072
+ content: `Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`,
3073
+ timestamp: new Date(),
3074
+ },
3075
+ ]);
3076
+ }
3077
+ return true;
3078
+ }
3038
3079
  if (c === "/wallet") {
3039
3080
  openWalletPicker();
3040
3081
  return true;
@@ -3731,6 +3772,9 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
3731
3772
  model,
3732
3773
  messages.length,
3733
3774
  messages,
3775
+ setSessionPickerList,
3776
+ setSessionPickerIndex,
3777
+ setShowSessionPicker,
3734
3778
  ]);
3735
3779
  const handleSlashMenuSelect = useCallback((item) => {
3736
3780
  setShowSlashMenuSync(false);
@@ -3905,34 +3949,27 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
3905
3949
  ]);
3906
3950
  break;
3907
3951
  case "sessions": {
3908
- // List recent sessions in this workspace so the user can pick one
3909
- // to resume on next launch (`muonroi-cli --session <id>`).
3910
- let body = "No prior sessions found in this workspace.";
3952
+ // Open the picker (delegates to the same path as typing `/sessions`)
3953
+ // so the user can pick a session and resume it directly instead of
3954
+ // having to remember the id + relaunch by hand.
3911
3955
  try {
3912
3956
  const { SessionStore } = require("../storage/sessions.js");
3913
- const store = new SessionStore(agent.getCwd());
3914
- const sessions = store.listRecentSessions(15);
3915
- if (sessions.length > 0) {
3916
- const lines = sessions.map((s, idx) => {
3917
- const ts = new Date(s.updatedAt).toLocaleString();
3918
- const title = s.title?.trim() || "(untitled)";
3919
- const truncTitle = title.length > 80 ? `${title.slice(0, 77)}...` : title;
3920
- return `${String(idx + 1).padStart(2)}. [${s.id}] ${ts} ${s.model}\n ${truncTitle}`;
3921
- });
3922
- body = [
3923
- "Recent sessions in this workspace:",
3924
- "",
3925
- ...lines,
3926
- "",
3927
- "Resume on next launch: muonroi-cli --session <id>",
3928
- "Or: muonroi-cli --session latest",
3929
- ].join("\n");
3930
- }
3957
+ const list = new SessionStore(agent.getCwd()).listRecentSessions(20);
3958
+ setSessionPickerList(list);
3959
+ setSessionPickerIndex(0);
3960
+ setShowSessionPicker(true);
3931
3961
  }
3932
3962
  catch (err) {
3933
- body = `Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`;
3963
+ console.error(`[session-picker] list failed: ${err?.message ?? err}`);
3964
+ setMessages((p) => [
3965
+ ...p,
3966
+ {
3967
+ type: "assistant",
3968
+ content: `Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`,
3969
+ timestamp: new Date(),
3970
+ },
3971
+ ]);
3934
3972
  }
3935
- setMessages((p) => [...p, { type: "assistant", content: body, timestamp: new Date() }]);
3936
3973
  break;
3937
3974
  }
3938
3975
  default: {
@@ -4060,12 +4097,16 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
4060
4097
  setModelPickerIndex,
4061
4098
  setModelSearchQuery,
4062
4099
  setShowModelPicker,
4100
+ setSessionPickerList,
4101
+ setSessionPickerIndex,
4102
+ setShowSessionPicker,
4063
4103
  ]);
4064
4104
  const blockPrompt = showConnectModal ||
4065
4105
  showTelegramTokenModal ||
4066
4106
  showTelegramPairModal ||
4067
4107
  showMcpModal ||
4068
4108
  showSandboxPicker ||
4109
+ showSessionPicker ||
4069
4110
  showWalletPicker ||
4070
4111
  !!pendingPaymentApproval ||
4071
4112
  showScheduleModal ||
@@ -5166,6 +5207,43 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
5166
5207
  }
5167
5208
  return;
5168
5209
  }
5210
+ if (showSessionPicker) {
5211
+ if (isEscapeKey(key)) {
5212
+ setShowSessionPicker(false);
5213
+ return;
5214
+ }
5215
+ if (key.name === "up") {
5216
+ setSessionPickerIndex((i) => Math.max(0, i - 1));
5217
+ return;
5218
+ }
5219
+ if (key.name === "down") {
5220
+ setSessionPickerIndex((i) => Math.min(Math.max(0, sessionPickerList.length - 1), i + 1));
5221
+ return;
5222
+ }
5223
+ if (key.name === "return") {
5224
+ const picked = sessionPickerList[sessionPickerIndex];
5225
+ if (!picked) {
5226
+ setShowSessionPicker(false);
5227
+ return;
5228
+ }
5229
+ // Close the modal first so the toast renders before the spawn.
5230
+ setShowSessionPicker(false);
5231
+ pushToast("info", `Resuming session ${picked.id.slice(-8)}… restarting CLI`);
5232
+ // Defer to the next tick so OpenTUI flushes the toast frame; then
5233
+ // spawn the child (which inherits the TTY) and exit this process.
5234
+ setTimeout(() => {
5235
+ try {
5236
+ relaunchWithSession(picked.id);
5237
+ }
5238
+ catch (err) {
5239
+ console.error(`[session-picker] relaunch failed: ${err?.message ?? err}`);
5240
+ pushToast("error", `Resume failed: ${err?.message ?? err}`);
5241
+ }
5242
+ }, 50);
5243
+ return;
5244
+ }
5245
+ return;
5246
+ }
5169
5247
  if (showModelPicker) {
5170
5248
  // Sub-modal: BW sync (password + provider picker phases).
5171
5249
  if (bwSync) {
@@ -5790,6 +5868,11 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
5790
5868
  setShowModelPicker,
5791
5869
  setModelPickerIndex,
5792
5870
  setModel,
5871
+ showSessionPicker,
5872
+ sessionPickerList,
5873
+ sessionPickerIndex,
5874
+ setShowSessionPicker,
5875
+ setSessionPickerIndex,
5793
5876
  ]);
5794
5877
  useKeyboard(handleKey);
5795
5878
  const handlePaste = useCallback((event) => {
@@ -6024,7 +6107,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
6024
6107
  : `💭 Thought for ${(lastReasoningElapsedMs / 1000).toFixed(1)}s` }) })), streamContent && (_jsx("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, children: _jsx(Markdown, { content: streamContent, t: t }) })), isProcessing && !streamContent && activeToolCalls.length === 0 && (_jsx(ShimmerText, { t: t, text: "Planning next moves" })), showPlanPanel && _jsx(PlanQuestionsPanel, { t: t, questions: planQuestions, state: pqs }), pendingPaymentApproval && _jsx(PaymentApprovalPanel, { t: t, payment: pendingPaymentApproval }), activeHaltCard && (_jsx(HaltRecoveryCard, { halt: activeHaltCard, selectedIndex: haltSelectedIndex, terminalCols: width, theme: t })), initNewForm && _jsx(InitNewFormCard, { state: initNewForm, terminalCols: width, theme: t }), pointToExistingForm && (_jsx(PointToExistingFormCard, { state: pointToExistingForm, terminalCols: width, theme: t })), councilProgress && (_jsx(Semantic, { id: "continue-as-council-progress", role: "log", name: "Council brainstorm", children: _jsx("box", { flexDirection: "column", borderStyle: "single", borderColor: councilProgress.status === "error" ? t.initFormError : t.text, padding: 1, marginTop: 1, children: _jsxs("text", { fg: t.text, children: [councilProgress.status === "running" && "Council brainstorming — writing spec.md...", councilProgress.status === "done" &&
6025
6108
  `Council brainstorm complete: ${councilProgress.specPath}${councilProgress.hasContent ? "" : " (no content — production council wiring deferred)"}`, councilProgress.status === "error" && `Council brainstorm failed: ${councilProgress.error}`] }) }) }))] }) }), btwState && _jsx(BtwOverlay, { state: btwState, theme: t }), _jsx("box", { flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) })] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 })] })] })) : (
6026
6109
  /* ── Home ───────────────────────────────────────── */
6027
- _jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsx("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?", typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), updateInfo?.hasUpdate && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: "#f59e0b", children: ["┃ Update available: v", startupConfig.version, " → v", updateInfo.latestVersion, " — run /update to install"] }) })), isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsx("text", { fg: "#f59e0b", children: "┃ Updating..." }) })), updateOutput && !isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: updateOutput.startsWith("Update complete") ? "#22c55e" : "#ef4444", children: ["┃ ", updateOutput] }) })), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: `v${startupConfig.version}` })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showUpdateModal && updateInfo && (_jsx(UpdateModal, { t: t, width: width, height: height, currentVersion: startupConfig.version, latestVersion: updateInfo.latestVersion })), showMcpModal && !showMcpEditor && (_jsx(McpBrowserModal, { t: t, width: width, height: height, selectedIndex: mcpModalIndex, searchQuery: mcpSearchQuery, rows: mcpRows })), showMcpEditor && (_jsx(McpEditorModal, { t: t, width: width, height: height, draft: mcpEditorDraft, focusedField: mcpEditorField, syncKey: mcpEditorSyncKey, error: mcpEditorError, title: editingMcpId ? "Edit MCP Server" : "Add MCP Server", labelRef: mcpLabelRef, urlRef: mcpUrlRef, headersRef: mcpHeadersRef, commandRef: mcpCommandRef, argsRef: mcpArgsRef, cwdRef: mcpCwdRef, envRef: mcpEnvRef, onSubmit: submitMcpEditor })), showScheduleModal && (_jsx(ScheduleBrowserModal, { t: t, width: width, height: height, selectedIndex: scheduleModalIndex, searchQuery: scheduleSearchQuery, rows: scheduleRows })), showAgentsModal && !showAgentsEditor && (_jsx(SubagentsBrowserModal, { t: t, width: width, height: height, selectedIndex: agentsModalIndex, searchQuery: agentsSearchQuery, rows: agentRows })), showAgentsEditor && (_jsx(SubagentEditorModal, { t: t, width: width, height: height, draft: agentsEditorDraft, focusedField: agentsEditorField, modelIndex: agentsEditorModelIndex, error: agentsEditorError, title: editingSubagent ? `Edit sub-agent: ${formatSubagentName(editingSubagent.name)}` : "Add sub-agent", nameRef: subagentNameRef, instructionRef: subagentInstructionRef, onSubmit: submitSubagentEditor, showRemoveHint: !!editingSubagent }, `subagent-editor-${agentsEditorSyncKey}`)), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels, reasoningEffortByModel: reasoningEffortByModel, configuredProviders: configuredProviders, disabledProviders: disabledProviders, disabledModels: disabledModels, defaultProvider: defaultProvider, focus: modelPickerFocus, providerChipIndex: providerChipIndex, providersWithKey: providersWithKey, apiKeyPrompt: apiKeyPrompt, bwSync: bwSync })), showWalletPicker && (_jsx(WalletPickerModal, { t: t, settings: walletSettings, walletInfo: walletDisplayInfo, focusIndex: walletFocusIndex, width: width, height: height })), showSandboxPicker && (_jsx(SandboxPickerModal, { t: t, currentMode: sandboxMode, settings: sandboxSettings, focusIndex: sandboxSettingsFocusIndex, editing: sandboxSettingsEditing, editBuffer: sandboxSettingsEditBuffer, width: width, height: height })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }) }));
6110
+ _jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsx("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?", typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), updateInfo?.hasUpdate && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: "#f59e0b", children: ["┃ Update available: v", startupConfig.version, " → v", updateInfo.latestVersion, " — run /update to install"] }) })), isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsx("text", { fg: "#f59e0b", children: "┃ Updating..." }) })), updateOutput && !isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: updateOutput.startsWith("Update complete") ? "#22c55e" : "#ef4444", children: ["┃ ", updateOutput] }) })), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: `v${startupConfig.version}` })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showUpdateModal && updateInfo && (_jsx(UpdateModal, { t: t, width: width, height: height, currentVersion: startupConfig.version, latestVersion: updateInfo.latestVersion })), showMcpModal && !showMcpEditor && (_jsx(McpBrowserModal, { t: t, width: width, height: height, selectedIndex: mcpModalIndex, searchQuery: mcpSearchQuery, rows: mcpRows })), showMcpEditor && (_jsx(McpEditorModal, { t: t, width: width, height: height, draft: mcpEditorDraft, focusedField: mcpEditorField, syncKey: mcpEditorSyncKey, error: mcpEditorError, title: editingMcpId ? "Edit MCP Server" : "Add MCP Server", labelRef: mcpLabelRef, urlRef: mcpUrlRef, headersRef: mcpHeadersRef, commandRef: mcpCommandRef, argsRef: mcpArgsRef, cwdRef: mcpCwdRef, envRef: mcpEnvRef, onSubmit: submitMcpEditor })), showScheduleModal && (_jsx(ScheduleBrowserModal, { t: t, width: width, height: height, selectedIndex: scheduleModalIndex, searchQuery: scheduleSearchQuery, rows: scheduleRows })), showAgentsModal && !showAgentsEditor && (_jsx(SubagentsBrowserModal, { t: t, width: width, height: height, selectedIndex: agentsModalIndex, searchQuery: agentsSearchQuery, rows: agentRows })), showAgentsEditor && (_jsx(SubagentEditorModal, { t: t, width: width, height: height, draft: agentsEditorDraft, focusedField: agentsEditorField, modelIndex: agentsEditorModelIndex, error: agentsEditorError, title: editingSubagent ? `Edit sub-agent: ${formatSubagentName(editingSubagent.name)}` : "Add sub-agent", nameRef: subagentNameRef, instructionRef: subagentInstructionRef, onSubmit: submitSubagentEditor, showRemoveHint: !!editingSubagent }, `subagent-editor-${agentsEditorSyncKey}`)), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels, reasoningEffortByModel: reasoningEffortByModel, configuredProviders: configuredProviders, disabledProviders: disabledProviders, disabledModels: disabledModels, defaultProvider: defaultProvider, focus: modelPickerFocus, providerChipIndex: providerChipIndex, providersWithKey: providersWithKey, apiKeyPrompt: apiKeyPrompt, bwSync: bwSync })), showSessionPicker && (_jsx(SessionPickerModal, { t: t, sessions: sessionPickerList, focusIndex: sessionPickerIndex, width: width, height: height })), showWalletPicker && (_jsx(WalletPickerModal, { t: t, settings: walletSettings, walletInfo: walletDisplayInfo, focusIndex: walletFocusIndex, width: width, height: height })), showSandboxPicker && (_jsx(SandboxPickerModal, { t: t, currentMode: sandboxMode, settings: sandboxSettings, focusIndex: sandboxSettingsFocusIndex, editing: sandboxSettingsEditing, editBuffer: sandboxSettingsEditBuffer, width: width, height: height })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }) }));
6028
6111
  }
6029
6112
  export { computeMcpRunInfo } from "./components/message-view.js";
6030
6113
  /* ── Slash Menu ──────────────────────────────────────────────── */
@@ -0,0 +1,14 @@
1
+ import type { SessionInfo } from "../../types/index.js";
2
+ /**
3
+ * State for the /sessions picker modal. Sessions are loaded lazily when the
4
+ * picker is opened (the SQLite query is cheap — ORDER BY updated_at LIMIT 20
5
+ * on an indexed column) so we do not pay for it on cold boot.
6
+ */
7
+ export declare function useSessionPicker(): {
8
+ showSessionPicker: boolean;
9
+ setShowSessionPicker: import("react").Dispatch<import("react").SetStateAction<boolean>>;
10
+ sessionPickerIndex: number;
11
+ setSessionPickerIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
12
+ sessions: SessionInfo[];
13
+ setSessions: import("react").Dispatch<import("react").SetStateAction<SessionInfo[]>>;
14
+ };
@@ -0,0 +1,20 @@
1
+ import { useState } from "react";
2
+ /**
3
+ * State for the /sessions picker modal. Sessions are loaded lazily when the
4
+ * picker is opened (the SQLite query is cheap — ORDER BY updated_at LIMIT 20
5
+ * on an indexed column) so we do not pay for it on cold boot.
6
+ */
7
+ export function useSessionPicker() {
8
+ const [showSessionPicker, setShowSessionPicker] = useState(false);
9
+ const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
10
+ const [sessions, setSessions] = useState([]);
11
+ return {
12
+ showSessionPicker,
13
+ setShowSessionPicker,
14
+ sessionPickerIndex,
15
+ setSessionPickerIndex,
16
+ sessions,
17
+ setSessions,
18
+ };
19
+ }
20
+ //# sourceMappingURL=use-session-picker.js.map
@@ -0,0 +1,14 @@
1
+ import type { SessionInfo } from "../../types/index.js";
2
+ import type { Theme } from "../theme.js";
3
+ /**
4
+ * Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
5
+ * row relaunches the CLI with `--session <id>` (see ui/utils/relaunch.ts) so
6
+ * the user does not need to remember the id or restart by hand.
7
+ */
8
+ export declare function SessionPickerModal({ t, sessions, focusIndex, width, height, }: {
9
+ t: Theme;
10
+ sessions: SessionInfo[];
11
+ focusIndex: number;
12
+ width: number;
13
+ height: number;
14
+ }): import("react").ReactNode;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { bottomAlignedModalTop } from "../utils/modal.js";
3
+ /**
4
+ * Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
5
+ * row relaunches the CLI with `--session <id>` (see ui/utils/relaunch.ts) so
6
+ * the user does not need to remember the id or restart by hand.
7
+ */
8
+ export function SessionPickerModal({ t, sessions, focusIndex, width, height, }) {
9
+ const panelWidth = Math.min(80, width - 6);
10
+ const rowCount = Math.max(sessions.length, 1);
11
+ // 4 chrome lines (title row + spacer + footer + paddings) + the rows
12
+ const contentHeight = rowCount + 4;
13
+ const maxH = Math.floor(height * 0.7);
14
+ const panelHeight = Math.min(contentHeight, maxH);
15
+ const top = bottomAlignedModalTop(height, panelHeight);
16
+ const overlayBg = "#000000cc";
17
+ return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Resume session" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("scrollbox", { flexGrow: 1, minHeight: 0, children: sessions.length === 0 ? (_jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "No prior sessions in this workspace." }) })) : (sessions.map((s, idx) => {
18
+ const focused = idx === focusIndex;
19
+ const ts = formatTimestamp(s.updatedAt);
20
+ const titleRaw = s.title?.trim() || "(untitled)";
21
+ const titleMax = Math.max(8, panelWidth - 38);
22
+ const title = titleRaw.length > titleMax ? `${titleRaw.slice(0, titleMax - 1)}…` : titleRaw;
23
+ const idShort = s.id.slice(-8);
24
+ return (_jsxs("box", { backgroundColor: focused ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, width: "100%", flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: focused ? t.selected : t.text, children: `${ts} ${title}` }), _jsx("text", { fg: focused ? t.primary : t.textMuted, children: `${s.model} ${idShort}` })] }, s.id));
25
+ })) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "↑↓ navigate · enter resume (restarts CLI) · esc cancel" }) })] }) }));
26
+ }
27
+ /**
28
+ * Compact MM-DD HH:MM timestamp for the picker rows. Trades the year for
29
+ * space — the picker is workspace-scoped + lists the latest 20 sessions, so
30
+ * a year boundary is rare and an obvious context.
31
+ */
32
+ function formatTimestamp(d) {
33
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
34
+ const dd = String(d.getDate()).padStart(2, "0");
35
+ const hh = String(d.getHours()).padStart(2, "0");
36
+ const min = String(d.getMinutes()).padStart(2, "0");
37
+ return `${mm}-${dd} ${hh}:${min}`;
38
+ }
39
+ //# sourceMappingURL=session-picker-modal.js.map