kimiflare 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7198,6 +7198,41 @@ function isNewer(local, remote) {
7198
7198
  }
7199
7199
  return false;
7200
7200
  }
7201
+ async function readOptionalDepVersion(name) {
7202
+ try {
7203
+ const here = dirname6(fileURLToPath2(import.meta.url));
7204
+ const candidate = join12(here, "..", "..", "node_modules", name, "package.json");
7205
+ const raw = await readFile7(candidate, "utf8");
7206
+ const parsed = JSON.parse(raw);
7207
+ return parsed.version ?? null;
7208
+ } catch {
7209
+ return null;
7210
+ }
7211
+ }
7212
+ async function fetchDistTagVersion(name, tag2) {
7213
+ try {
7214
+ const res = await fetch(`https://registry.npmjs.org/${name}/${tag2}`, {
7215
+ headers: { "User-Agent": getUserAgent(), Accept: "application/json" }
7216
+ });
7217
+ if (!res.ok) return null;
7218
+ const data = await res.json();
7219
+ return data.version ?? null;
7220
+ } catch {
7221
+ return null;
7222
+ }
7223
+ }
7224
+ async function checkOptionalDependency(name, tag2) {
7225
+ const localVersion = await readOptionalDepVersion(name);
7226
+ if (!localVersion) {
7227
+ return { name, hasUpdate: false, localVersion: null, latestVersion: null };
7228
+ }
7229
+ const latestVersion = await fetchDistTagVersion(name, tag2);
7230
+ if (!latestVersion) {
7231
+ return { name, hasUpdate: false, localVersion, latestVersion: null };
7232
+ }
7233
+ const hasUpdate = isNewer(localVersion, latestVersion);
7234
+ return { name, hasUpdate, localVersion, latestVersion };
7235
+ }
7201
7236
  async function checkForUpdate(force = false) {
7202
7237
  const localVersion = await readLocalVersion();
7203
7238
  if (!localVersion) return { hasUpdate: false, localVersion: null, latestVersion: null };
@@ -12236,6 +12271,8 @@ var init_supervisor = __esm({
12236
12271
  _phase = "idle";
12237
12272
  _killRequested = false;
12238
12273
  _activeWorkers = /* @__PURE__ */ new Map();
12274
+ /** Injectable LLM client for synthesis (overridable in tests). */
12275
+ _runKimi = runKimi;
12239
12276
  /** Coordinator-side MemoryManager for proxying memories to workers */
12240
12277
  memoryManager = null;
12241
12278
  /** Coordinator-side LspManager for proxying LSP context to workers */
@@ -12631,17 +12668,8 @@ ${w.context ?? ""}` : w.context ?? "",
12631
12668
  }
12632
12669
  return results;
12633
12670
  }
12634
- /** Synthesize findings from multiple workers into a unified execution plan.
12635
- *
12636
- * Steps:
12637
- * 1. Deduplicate findings by topic (case-insensitive), keeping the highest-confidence version.
12638
- * 2. Detect conflicts — same topic with different recommendations.
12639
- * 3. Tie-breaker: when conflicting recommendations exist, prefer the one from
12640
- * the worker with higher average confidence.
12641
- * 4. Produce a markdown execution plan the coordinator can present to the user
12642
- * or feed into an executor worker.
12643
- */
12644
- synthesizeFindings(results) {
12671
+ /** Heuristic synthesis exact legacy behavior, preserved as fallback. */
12672
+ synthesizeFindingsHeuristic(results) {
12645
12673
  const allFindings = results.flatMap((r) => r.findings);
12646
12674
  const allRecommendations = results.flatMap((r) => r.recommendations);
12647
12675
  const CONFIDENCE_SCORE = { high: 3, medium: 2, low: 1 };
@@ -12710,6 +12738,119 @@ ${w.context ?? ""}` : w.context ?? "",
12710
12738
  recommendations: resolvedRecommendations
12711
12739
  };
12712
12740
  }
12741
+ /** LLM-based synthesis with graceful fallback to heuristic. */
12742
+ async synthesizeFindingsLlm(results, opts2) {
12743
+ const MAX_RAW_OUTPUT = 2e3;
12744
+ const MAX_REASONING = 2e3;
12745
+ const workerBlocks = results.map((r, i) => {
12746
+ const findings = r.findings.map(
12747
+ (f) => ` - Topic: ${f.topic}
12748
+ Summary: ${f.summary}
12749
+ Confidence: ${f.confidence}
12750
+ Relevance: ${f.relevance}
12751
+ Sources: ${f.sources.join(", ") || "none"}`
12752
+ ).join("\n");
12753
+ const recs = r.recommendations.map((rec) => ` - ${rec}`).join("\n") || " (none)";
12754
+ const reasoning = r.reasoning ? r.reasoning.slice(0, MAX_REASONING) : "(none)";
12755
+ const rawOutput = r.rawOutput ? r.rawOutput.slice(0, MAX_RAW_OUTPUT) : "(none)";
12756
+ return [
12757
+ `--- Worker ${r.workerId || `w${i + 1}`} ---`,
12758
+ `Task: ${r.task}`,
12759
+ `Status: ${r.status}`,
12760
+ `Findings:
12761
+ ${findings || " (none)"}`,
12762
+ `Recommendations:
12763
+ ${recs}`,
12764
+ `Reasoning:
12765
+ ${reasoning}`,
12766
+ `Raw Output (truncated):
12767
+ ${rawOutput}`
12768
+ ].join("\n");
12769
+ });
12770
+ const userContent = [
12771
+ opts2.prompt ? `Original user request:
12772
+ ${opts2.prompt}` : "",
12773
+ "",
12774
+ "Worker outputs:",
12775
+ workerBlocks.join("\n\n"),
12776
+ "",
12777
+ "Instructions:",
12778
+ "1. Synthesize the worker findings into a coherent execution plan.",
12779
+ "2. Detect and resolve any conflicts between workers.",
12780
+ "3. Cite sources inline using [worker: <id>] notation.",
12781
+ "4. Return ONLY valid JSON in this exact shape (no markdown fences):",
12782
+ '{"plan":"markdown plan","conflicts":["string"],"recommendations":["string"],"reasoning":"optional string"}'
12783
+ ].filter(Boolean).join("\n");
12784
+ const messages = [
12785
+ {
12786
+ role: "system",
12787
+ content: "You are a synthesis engine. Combine findings from multiple research workers into a single coherent execution plan. Be concise. Return only valid JSON."
12788
+ },
12789
+ { role: "user", content: userContent }
12790
+ ];
12791
+ let text = "";
12792
+ const events = this._runKimi({
12793
+ accountId: opts2.accountId,
12794
+ apiToken: opts2.apiToken,
12795
+ model: opts2.model,
12796
+ messages,
12797
+ temperature: 0.2,
12798
+ maxCompletionTokens: 4096,
12799
+ reasoningEffort: "low",
12800
+ gateway: opts2.gateway,
12801
+ signal: opts2.signal
12802
+ });
12803
+ for await (const ev of events) {
12804
+ if (ev.type === "text") {
12805
+ text += ev.delta;
12806
+ opts2.onDelta?.(ev.delta);
12807
+ }
12808
+ }
12809
+ const cleaned = text.replace(/```(?:json)?\s*/gi, "").replace(/```\s*$/gi, "").trim();
12810
+ const parsed = JSON.parse(cleaned);
12811
+ if (!parsed.plan || !Array.isArray(parsed.conflicts) || !Array.isArray(parsed.recommendations)) {
12812
+ throw new Error("LLM synthesis returned malformed JSON");
12813
+ }
12814
+ return {
12815
+ plan: parsed.plan,
12816
+ conflicts: parsed.conflicts,
12817
+ recommendations: parsed.recommendations
12818
+ };
12819
+ }
12820
+ /** Synthesize findings from multiple workers into a unified execution plan.
12821
+ *
12822
+ * Uses LLM-based synthesis by default (configurable via synthesisStrategy).
12823
+ * Falls back to the heuristic path when credentials are missing, the strategy
12824
+ * demands it, or the LLM call fails.
12825
+ */
12826
+ async synthesizeFindings(results, opts2) {
12827
+ const strategy = opts2?.strategy ?? "llm";
12828
+ const disableLlm = opts2?.disableLlmSynthesis ?? false;
12829
+ const hasCreds = !!opts2?.accountId && !!opts2?.apiToken;
12830
+ const useHeuristic = !hasCreds || strategy === "heuristic" || disableLlm;
12831
+ if (useHeuristic) {
12832
+ return this.synthesizeFindingsHeuristic(results);
12833
+ }
12834
+ try {
12835
+ const llmResult = await this.synthesizeFindingsLlm(results, {
12836
+ prompt: opts2?.prompt,
12837
+ accountId: opts2.accountId,
12838
+ apiToken: opts2.apiToken,
12839
+ model: opts2?.model ?? "@cf/moonshotai/kimi-k2.5",
12840
+ gateway: opts2?.gateway,
12841
+ signal: opts2?.signal,
12842
+ onDelta: opts2?.onDelta
12843
+ });
12844
+ return llmResult;
12845
+ } catch (err) {
12846
+ const msg = err instanceof Error ? err.message : String(err);
12847
+ logger.warn("supervisor:synthesis_llm_failed", { error: msg });
12848
+ if (strategy === "hybrid") {
12849
+ return this.synthesizeFindingsHeuristic(results);
12850
+ }
12851
+ throw err;
12852
+ }
12853
+ }
12713
12854
  /** Automatically spawn research workers for a heavy prompt.
12714
12855
  *
12715
12856
  * Decomposes the prompt into 2-4 parallel research tasks using a simple
@@ -12735,7 +12876,22 @@ ${w.context ?? ""}` : w.context ?? "",
12735
12876
  try {
12736
12877
  const results = await this.spawnWorkers(workers, onUpdate, signal);
12737
12878
  onPhaseChange?.("synthesizing");
12738
- const synth = this.synthesizeFindings(results);
12879
+ const gateway = cfg?.aiGatewayId ? {
12880
+ id: cfg.aiGatewayId,
12881
+ cacheTtl: cfg.aiGatewayCacheTtl,
12882
+ skipCache: cfg.aiGatewaySkipCache,
12883
+ metadata: { feature: "synthesis", ...cfg.aiGatewayMetadata ?? {} }
12884
+ } : void 0;
12885
+ const synth = await this.synthesizeFindings(results, {
12886
+ prompt,
12887
+ accountId: cfg?.accountId,
12888
+ apiToken: cfg?.apiToken,
12889
+ model: cfg?.synthesisModel,
12890
+ gateway,
12891
+ signal,
12892
+ strategy: cfg?.synthesisStrategy,
12893
+ disableLlmSynthesis: cfg?.disableLlmSynthesis
12894
+ });
12739
12895
  const cfg2 = await loadConfig().catch(() => null);
12740
12896
  const autoExecute = cfg2?.autoExecute ?? /^(1|true|yes|on)$/i.test(process.env.KIMIFLARE_AUTO_EXECUTE ?? "");
12741
12897
  if (!autoExecute || synth.recommendations.length === 0) {
@@ -13876,6 +14032,30 @@ Do not include speculation. Do not include chat-style pleasantries. Use short bu
13876
14032
  }
13877
14033
  });
13878
14034
 
14035
+ // src/agent/distill.ts
14036
+ function distillSessionPlan(messages) {
14037
+ for (let i = messages.length - 1; i >= 0; i--) {
14038
+ const m = messages[i];
14039
+ if (m?.role !== "assistant") continue;
14040
+ let text = "";
14041
+ if (typeof m.content === "string") {
14042
+ text = m.content;
14043
+ } else if (Array.isArray(m.content)) {
14044
+ text = m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
14045
+ }
14046
+ text = text.trim();
14047
+ if (text.length > 20) {
14048
+ return text;
14049
+ }
14050
+ }
14051
+ return null;
14052
+ }
14053
+ var init_distill = __esm({
14054
+ "src/agent/distill.ts"() {
14055
+ "use strict";
14056
+ }
14057
+ });
14058
+
13879
14059
  // src/ui/greetings.ts
13880
14060
  function pick(arr) {
13881
14061
  if (arr.length === 0) throw new Error("pick() called with empty array");
@@ -14740,6 +14920,40 @@ var init_theme = __esm({
14740
14920
  }
14741
14921
  });
14742
14922
 
14923
+ // src/util/clipboard.ts
14924
+ import { execSync as execSync3 } from "child_process";
14925
+ import { platform as platform3 } from "os";
14926
+ function writeToClipboard(text) {
14927
+ const os2 = platform3();
14928
+ try {
14929
+ if (os2 === "darwin") {
14930
+ execSync3("pbcopy", { input: text, timeout: 5e3 });
14931
+ return { success: true, message: "Copied to clipboard" };
14932
+ }
14933
+ if (os2 === "win32") {
14934
+ execSync3("clip", { input: text, timeout: 5e3 });
14935
+ return { success: true, message: "Copied to clipboard" };
14936
+ }
14937
+ try {
14938
+ execSync3("xclip -selection clipboard", { input: text, timeout: 5e3 });
14939
+ return { success: true, message: "Copied to clipboard" };
14940
+ } catch {
14941
+ execSync3("xsel --clipboard --input", { input: text, timeout: 5e3 });
14942
+ return { success: true, message: "Copied to clipboard" };
14943
+ }
14944
+ } catch {
14945
+ return {
14946
+ success: false,
14947
+ message: "Clipboard not available \u2014 plan will be shown below"
14948
+ };
14949
+ }
14950
+ }
14951
+ var init_clipboard = __esm({
14952
+ "src/util/clipboard.ts"() {
14953
+ "use strict";
14954
+ }
14955
+ });
14956
+
14743
14957
  // src/cloud/ai-gateway-api.ts
14744
14958
  function baseUrl(accountId) {
14745
14959
  return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai-gateway/gateways`;
@@ -15967,11 +16181,11 @@ __export(ui_mode_exports, {
15967
16181
  runCamouflageOnboarding: () => runCamouflageOnboarding,
15968
16182
  runUiMode: () => runUiMode
15969
16183
  });
15970
- import { execSync as execSync3, spawn as spawn5 } from "child_process";
16184
+ import { execSync as execSync4, spawn as spawn5 } from "child_process";
15971
16185
  import { appendFileSync, openSync } from "fs";
15972
- import { readdir as readdir7 } from "fs/promises";
16186
+ import { readdir as readdir7, unlink as unlink4 } from "fs/promises";
15973
16187
  import { join as join28, relative as relative6 } from "path";
15974
- import { platform as platform3 } from "os";
16188
+ import { platform as platform4 } from "os";
15975
16189
  function kimiLog(payload) {
15976
16190
  if (!KIMI_LOG_PATH) return;
15977
16191
  try {
@@ -16088,6 +16302,17 @@ ${err instanceof Error ? err.message : err}`);
16088
16302
  }
16089
16303
  } catch {
16090
16304
  }
16305
+ try {
16306
+ const dep = await checkOptionalDependency("camouflage-tui", "beta");
16307
+ if (dep.hasUpdate && dep.latestVersion) {
16308
+ cam.send("ShowToast", {
16309
+ text: `camouflage-tui update available: ${dep.localVersion} \u2192 ${dep.latestVersion} \xB7 run npm update camouflage-tui`,
16310
+ kind: "info",
16311
+ ttl_ms: 6e3
16312
+ });
16313
+ }
16314
+ } catch {
16315
+ }
16091
16316
  })();
16092
16317
  cam.send("ShowToast", {
16093
16318
  text: "EXPERIMENTAL \u2014 switch back any time with `kimiflare --ui ink`",
@@ -17504,20 +17729,41 @@ Executor opened PR: ${prUrl}` : plan });
17504
17729
  return true;
17505
17730
  }
17506
17731
  case "update": {
17507
- cam.send("ShowToast", { text: "checking for updates\u2026", kind: "info", ttl_ms: 1500 });
17508
- try {
17509
- const r = await checkForUpdate(true);
17510
- if (r.hasUpdate && r.latestVersion) {
17511
- cam.send("ShowToast", {
17512
- text: `update available: ${r.localVersion} \u2192 ${r.latestVersion}. Run: npm i -g kimiflare@latest`,
17513
- kind: "success",
17514
- ttl_ms: 5e3
17515
- });
17516
- } else {
17517
- cam.send("ShowToast", { text: `up to date (${r.localVersion ?? "unknown"})`, kind: "info", ttl_ms: 2500 });
17732
+ const updateArg = (rest[0] ?? "").toLowerCase();
17733
+ if (updateArg === "camouflage") {
17734
+ cam.send("ShowToast", { text: "checking camouflage-tui for updates\u2026", kind: "info", ttl_ms: 1500 });
17735
+ try {
17736
+ const dep = await checkOptionalDependency("camouflage-tui", "beta");
17737
+ if (dep.hasUpdate && dep.latestVersion) {
17738
+ cam.send("ShowToast", {
17739
+ text: `camouflage-tui update available: ${dep.localVersion} \u2192 ${dep.latestVersion}. Run: npm update camouflage-tui`,
17740
+ kind: "success",
17741
+ ttl_ms: 5e3
17742
+ });
17743
+ } else if (dep.localVersion) {
17744
+ cam.send("ShowToast", { text: `camouflage-tui up to date (${dep.localVersion})`, kind: "info", ttl_ms: 2500 });
17745
+ } else {
17746
+ cam.send("ShowToast", { text: "camouflage-tui is not installed", kind: "info", ttl_ms: 2500 });
17747
+ }
17748
+ } catch (err) {
17749
+ cam.send("ShowToast", { text: `camouflage-tui update check failed: ${err instanceof Error ? err.message : String(err)}`, kind: "error", ttl_ms: 3e3 });
17750
+ }
17751
+ } else {
17752
+ cam.send("ShowToast", { text: "checking for updates\u2026", kind: "info", ttl_ms: 1500 });
17753
+ try {
17754
+ const r = await checkForUpdate(true);
17755
+ if (r.hasUpdate && r.latestVersion) {
17756
+ cam.send("ShowToast", {
17757
+ text: `update available: ${r.localVersion} \u2192 ${r.latestVersion}. Run: npm i -g kimiflare@latest`,
17758
+ kind: "success",
17759
+ ttl_ms: 5e3
17760
+ });
17761
+ } else {
17762
+ cam.send("ShowToast", { text: `up to date (${r.localVersion ?? "unknown"})`, kind: "info", ttl_ms: 2500 });
17763
+ }
17764
+ } catch (err) {
17765
+ cam.send("ShowToast", { text: `update check failed: ${err instanceof Error ? err.message : String(err)}`, kind: "error", ttl_ms: 3e3 });
17518
17766
  }
17519
- } catch (err) {
17520
- cam.send("ShowToast", { text: `update check failed: ${err instanceof Error ? err.message : String(err)}`, kind: "error", ttl_ms: 3e3 });
17521
17767
  }
17522
17768
  return true;
17523
17769
  }
@@ -17859,8 +18105,43 @@ Executor opened PR: ${prUrl}` : plan });
17859
18105
  ttl_ms: 4e3
17860
18106
  });
17861
18107
  return true;
18108
+ case "fresh": {
18109
+ if (currentPhase !== "idle") {
18110
+ cam.send("ShowToast", { text: "can't /fresh while model is running \u2014 press Esc to interrupt first", kind: "warn", ttl_ms: 2500 });
18111
+ return true;
18112
+ }
18113
+ const plan = distillSessionPlan(messages);
18114
+ if (!plan) {
18115
+ cam.send("ShowToast", { text: "No plan found to start fresh with.", kind: "error", ttl_ms: 2500 });
18116
+ return true;
18117
+ }
18118
+ const clipResult = writeToClipboard(plan);
18119
+ const systemMessages = messages.filter((m) => m.role === "system");
18120
+ messages.length = 0;
18121
+ messages.push(...systemMessages);
18122
+ sessionCostUsd = 0;
18123
+ promptTokens = 0;
18124
+ cachedTokens = 0;
18125
+ completionTokens = 0;
18126
+ cam.send("TranscriptCleared", {});
18127
+ cam.send("StatusUpdate", {
18128
+ segments: { tokens: "in 0", cost: "$0.00", elapsed: "" }
18129
+ });
18130
+ messages.push({ role: "user", content: plan });
18131
+ cam.send("ShowToast", {
18132
+ text: clipResult.success ? "Plan copied to clipboard. Starting fresh session with plan only\u2026" : "Clipboard unavailable. Starting fresh session with plan only\u2026",
18133
+ kind: "info",
18134
+ ttl_ms: 3e3
18135
+ });
18136
+ if (!clipResult.success) {
18137
+ cam.send("UserMessageCreated", { text: "--- Plan ---\n" + plan });
18138
+ }
18139
+ return true;
18140
+ }
17862
18141
  case "logout": {
17863
- cam.send("ShowToast", { text: "Cloud has been removed; nothing to log out of.", kind: "info", ttl_ms: 2500 });
18142
+ unlink4(configPath()).catch(() => {
18143
+ });
18144
+ cam.send("ShowToast", { text: `credentials cleared from ${configPath()}`, kind: "success", ttl_ms: 2500 });
17864
18145
  return true;
17865
18146
  }
17866
18147
  case "remote": {
@@ -17969,7 +18250,7 @@ function formatShortDate(iso) {
17969
18250
  }
17970
18251
  }
17971
18252
  function openBrowser(url) {
17972
- const cmd = platform3() === "darwin" ? "open" : platform3() === "win32" ? "start" : "xdg-open";
18253
+ const cmd = platform4() === "darwin" ? "open" : platform4() === "win32" ? "start" : "xdg-open";
17973
18254
  const child = spawn5(cmd, [url], { detached: true, stdio: "ignore" });
17974
18255
  child.unref();
17975
18256
  }
@@ -17989,7 +18270,7 @@ function formatElapsed2(secs) {
17989
18270
  }
17990
18271
  function tryGitBranch2() {
17991
18272
  try {
17992
- const out = execSync3("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
18273
+ const out = execSync4("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
17993
18274
  encoding: "utf8",
17994
18275
  timeout: 200
17995
18276
  }).trim();
@@ -18099,9 +18380,11 @@ var init_ui_mode = __esm({
18099
18380
  init_builtins();
18100
18381
  init_sessions();
18101
18382
  init_llm_summarize();
18383
+ init_distill();
18102
18384
  init_greetings();
18103
18385
  init_theme();
18104
18386
  init_update_check();
18387
+ init_clipboard();
18105
18388
  init_pricing();
18106
18389
  init_config();
18107
18390
  init_ai_gateway_api();
@@ -19524,12 +19807,19 @@ var init_chat = __esm({
19524
19807
  for (const [sig, count] of toolCounts) {
19525
19808
  if (count >= 3) repeatedSigs.add(sig);
19526
19809
  }
19810
+ let lastAssistantIndex = -1;
19811
+ for (let i = events.length - 1; i >= 0; i--) {
19812
+ if (events[i].kind === "assistant") {
19813
+ lastAssistantIndex = i;
19814
+ break;
19815
+ }
19816
+ }
19527
19817
  return /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", children: events.map((e, i) => {
19528
19818
  const prev = events[i - 1];
19529
19819
  const showSeparator = !!(prev && (e.kind === "user" && prev.kind !== "user" || e.kind === "assistant" && prev.kind !== "assistant" && prev.kind !== "tool"));
19530
19820
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
19531
19821
  showSeparator && /* @__PURE__ */ jsx6(Box5, { marginY: 1, children: /* @__PURE__ */ jsx6(Text5, { color: theme.info.color, children: "\u2500".repeat(40) }) }),
19532
- /* @__PURE__ */ jsx6(EventView, { evt: e, showReasoning, verbose, repeatedSigs, intentTier })
19822
+ /* @__PURE__ */ jsx6(EventView, { evt: e, showReasoning, verbose, repeatedSigs, intentTier, isLastAssistant: i === lastAssistantIndex })
19533
19823
  ] }, e.key);
19534
19824
  }) });
19535
19825
  });
@@ -19538,7 +19828,8 @@ var init_chat = __esm({
19538
19828
  showReasoning,
19539
19829
  verbose,
19540
19830
  repeatedSigs,
19541
- intentTier
19831
+ intentTier,
19832
+ isLastAssistant
19542
19833
  }) {
19543
19834
  const theme = useTheme();
19544
19835
  if (evt.kind === "user") {
@@ -19583,7 +19874,7 @@ var init_chat = __esm({
19583
19874
  evt.reasoning.length > 400 ? evt.reasoning.slice(0, 400) + "\u2026" : evt.reasoning
19584
19875
  ] }) }) : null,
19585
19876
  evt.text ? /* @__PURE__ */ jsx6(MD, { text: evt.text }) : null,
19586
- evt.streaming && /* @__PURE__ */ jsx6(Text5, { color: theme.spinner, children: /* @__PURE__ */ jsx6(Spinner2, { type: "dots" }) })
19877
+ evt.streaming && isLastAssistant && /* @__PURE__ */ jsx6(Text5, { color: theme.spinner, children: /* @__PURE__ */ jsx6(Spinner2, { type: "dots" }) })
19587
19878
  ] })
19588
19879
  ] }) });
19589
19880
  }
@@ -20284,7 +20575,7 @@ var init_source = __esm({
20284
20575
 
20285
20576
  // src/ui/text-input.tsx
20286
20577
  import { useState as useState3, useEffect as useEffect3, useRef } from "react";
20287
- import { Text as Text7, useInput } from "ink";
20578
+ import { Text as Text7, useInput, usePaste } from "ink";
20288
20579
  import { jsx as jsx8 } from "react/jsx-runtime";
20289
20580
  function shouldTreatAsPaste(input) {
20290
20581
  if (input.length >= PASTE_CHAR_THRESHOLD) return true;
@@ -20301,6 +20592,9 @@ function makePastePreview(input, lines, id) {
20301
20592
  function countLines(s) {
20302
20593
  return s.split("\n").length;
20303
20594
  }
20595
+ function sanitizeInput(input) {
20596
+ return input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\t/g, " ");
20597
+ }
20304
20598
  function findWordBoundaryForward(text, pos) {
20305
20599
  while (pos < text.length && /\w/.test(text[pos])) pos++;
20306
20600
  while (pos < text.length && !/\w/.test(text[pos])) pos++;
@@ -20338,13 +20632,51 @@ function CustomTextInput({
20338
20632
  };
20339
20633
  const pastesRef = useRef(/* @__PURE__ */ new Map());
20340
20634
  const pasteCounterRef = useRef(0);
20635
+ const prevValueRef = useRef(value);
20636
+ const cursorOffsetRef = useRef(cursorOffset);
20637
+ cursorOffsetRef.current = cursorOffset;
20341
20638
  useEffect3(() => {
20342
20639
  if (!focus) return;
20343
- const next = cursorOffset > value.length ? value.length : cursorOffset;
20344
- if (next !== cursorOffset) {
20640
+ const prevValue = prevValueRef.current;
20641
+ prevValueRef.current = value;
20642
+ if (value === prevValue) return;
20643
+ const currentCursor = cursorOffsetRef.current;
20644
+ if (currentCursor === prevValue.length) {
20645
+ if (currentCursor !== value.length) {
20646
+ setCursorOffset(value.length);
20647
+ }
20648
+ return;
20649
+ }
20650
+ const next = currentCursor > value.length ? value.length : currentCursor;
20651
+ if (next !== currentCursor) {
20345
20652
  setCursorOffset(next);
20346
20653
  }
20347
- }, [value, focus, cursorOffset]);
20654
+ }, [value, focus]);
20655
+ const handleInsert = (rawInput) => {
20656
+ let toInsert = sanitizeInput(rawInput);
20657
+ if (enablePaste && shouldTreatAsPaste(toInsert)) {
20658
+ const lines = countLines(toInsert);
20659
+ const id = ++pasteCounterRef.current;
20660
+ const placeholder = makePastePreview(toInsert, lines, id);
20661
+ pastesRef.current.set(placeholder, toInsert);
20662
+ toInsert = placeholder;
20663
+ }
20664
+ const nextValue = value.slice(0, cursorOffset) + toInsert + value.slice(cursorOffset);
20665
+ const nextCursor = cursorOffset + toInsert.length;
20666
+ if (nextCursor !== cursorOffset) {
20667
+ setCursorOffset(nextCursor);
20668
+ }
20669
+ if (nextValue !== value) {
20670
+ onChange(nextValue);
20671
+ }
20672
+ };
20673
+ usePaste(
20674
+ (input) => {
20675
+ if (!focus || !enablePaste) return;
20676
+ handleInsert(input);
20677
+ },
20678
+ { isActive: focus && enablePaste }
20679
+ );
20348
20680
  useInput(
20349
20681
  (input, key) => {
20350
20682
  if (!focus) return;
@@ -20469,12 +20801,13 @@ function CustomTextInput({
20469
20801
  didDelete = true;
20470
20802
  nextValue = value.slice(0, cursorOffset);
20471
20803
  } else if (input.length > 0 && !key.ctrl && !key.meta) {
20472
- let toInsert = input;
20473
- if (enablePaste && shouldTreatAsPaste(input)) {
20474
- const lines = countLines(input);
20804
+ const sanitized = sanitizeInput(input);
20805
+ let toInsert = sanitized;
20806
+ if (enablePaste && shouldTreatAsPaste(toInsert)) {
20807
+ const lines = countLines(toInsert);
20475
20808
  const id = ++pasteCounterRef.current;
20476
- const placeholder = makePastePreview(input, lines, id);
20477
- pastesRef.current.set(placeholder, input);
20809
+ const placeholder = makePastePreview(toInsert, lines, id);
20810
+ pastesRef.current.set(placeholder, toInsert);
20478
20811
  toInsert = placeholder;
20479
20812
  }
20480
20813
  nextValue = value.slice(0, cursorOffset) + toInsert + value.slice(cursorOffset);
@@ -20496,10 +20829,9 @@ function CustomTextInput({
20496
20829
  );
20497
20830
  const displayValue = mask ? mask.repeat(value.length) : value;
20498
20831
  let renderedValue = "";
20499
- let i = 0;
20500
- for (const char of displayValue) {
20832
+ for (let i = 0; i < displayValue.length; i++) {
20833
+ const char = displayValue[i];
20501
20834
  renderedValue += i === cursorOffset ? source_default.inverse(char) : char;
20502
- i++;
20503
20835
  }
20504
20836
  if (displayValue.length === 0) {
20505
20837
  renderedValue = source_default.inverse(" ");
@@ -20530,7 +20862,7 @@ var init_text_input = __esm({
20530
20862
  // src/ui/permission.tsx
20531
20863
  import { useState as useState4, useCallback } from "react";
20532
20864
  import { Box as Box7, Text as Text8, useInput as useInput2 } from "ink";
20533
- import { platform as platform4 } from "os";
20865
+ import { platform as platform5 } from "os";
20534
20866
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
20535
20867
  function formatSelection(label, shortcut) {
20536
20868
  return `${label} [${MOD_KEY}+${shortcut}]`;
@@ -20694,7 +21026,7 @@ var init_permission = __esm({
20694
21026
  { value: { decision: "deny", scope: "once" }, label: "Something else", key: 3 }
20695
21027
  ];
20696
21028
  DENY = { decision: "deny", scope: "once" };
20697
- MOD_KEY = platform4() === "darwin" ? "\u2325" : "Alt";
21029
+ MOD_KEY = platform5() === "darwin" ? "\u2325" : "Alt";
20698
21030
  }
20699
21031
  });
20700
21032
 
@@ -23620,8 +23952,9 @@ function useModalHost() {
23620
23952
  const [showGatewayPicker, setShowGatewayPicker] = useState15(false);
23621
23953
  const [showSkillsPicker, setShowSkillsPicker] = useState15(false);
23622
23954
  const [showShellPicker, setShowShellPicker] = useState15(false);
23955
+ const [showPlanCompletePicker, setShowPlanCompletePicker] = useState15(false);
23623
23956
  const flags = useMemo4(() => {
23624
- const hasFullscreenModal = commandWizard !== null || commandPicker !== null || commandToDelete !== null || showCommandList || showLspWizard || showThemePicker || showUiPicker || showModelPicker || showModePicker || keyEntryFor !== null || billingChooserFor !== null || unifiedProbeFor !== null || showRemoteDashboard || showInboxModal || showMultiAgentModal || showHooksDashboard || showHelpMenu || showMemoryPicker || showGatewayPicker || showSkillsPicker || showShellPicker;
23957
+ const hasFullscreenModal = commandWizard !== null || commandPicker !== null || commandToDelete !== null || showCommandList || showLspWizard || showThemePicker || showUiPicker || showModelPicker || showModePicker || keyEntryFor !== null || billingChooserFor !== null || unifiedProbeFor !== null || showRemoteDashboard || showInboxModal || showMultiAgentModal || showHooksDashboard || showHelpMenu || showMemoryPicker || showGatewayPicker || showSkillsPicker || showShellPicker || showPlanCompletePicker;
23625
23958
  const hasOverlayModal = limitModal !== null || loopModal !== null;
23626
23959
  return {
23627
23960
  hasFullscreenModal,
@@ -23650,6 +23983,7 @@ function useModalHost() {
23650
23983
  showGatewayPicker,
23651
23984
  showSkillsPicker,
23652
23985
  showShellPicker,
23986
+ showPlanCompletePicker,
23653
23987
  limitModal,
23654
23988
  loopModal
23655
23989
  ]);
@@ -23700,6 +24034,8 @@ function useModalHost() {
23700
24034
  setShowSkillsPicker,
23701
24035
  showShellPicker,
23702
24036
  setShowShellPicker,
24037
+ showPlanCompletePicker,
24038
+ setShowPlanCompletePicker,
23703
24039
  ...flags
23704
24040
  };
23705
24041
  }
@@ -25582,10 +25918,10 @@ var init_inbox_modal = __esm({
25582
25918
  });
25583
25919
 
25584
25920
  // src/ui/app-helpers.ts
25585
- import { execSync as execSync4, spawn as spawn7 } from "child_process";
25921
+ import { execSync as execSync5, spawn as spawn7 } from "child_process";
25586
25922
  import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync5 } from "fs";
25587
25923
  import { join as join30 } from "path";
25588
- import { platform as platform5 } from "os";
25924
+ import { platform as platform6 } from "os";
25589
25925
  function buildFilePickerIgnoreList(cwd) {
25590
25926
  const hardcoded = [
25591
25927
  // Dependencies
@@ -25704,7 +26040,7 @@ function gatewayUsageLookupFromConfig(cfg, meta) {
25704
26040
  };
25705
26041
  }
25706
26042
  function openBrowser2(url) {
25707
- const cmd = platform5() === "darwin" ? "open" : platform5() === "win32" ? "start" : "xdg-open";
26043
+ const cmd = platform6() === "darwin" ? "open" : platform6() === "win32" ? "start" : "xdg-open";
25708
26044
  const child = spawn7(cmd, [url], { detached: true, stdio: "ignore" });
25709
26045
  child.unref();
25710
26046
  }
@@ -25714,7 +26050,7 @@ function detectGitHubRepo(cachedRepo) {
25714
26050
  if (parts.length === 2) return { owner: parts[0], name: parts[1] };
25715
26051
  }
25716
26052
  try {
25717
- const remoteUrl = execSync4("git remote get-url origin", { cwd: process.cwd(), encoding: "utf8" }).trim();
26053
+ const remoteUrl = execSync5("git remote get-url origin", { cwd: process.cwd(), encoding: "utf8" }).trim();
25718
26054
  const httpsMatch = remoteUrl.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
25719
26055
  if (httpsMatch) return { owner: httpsMatch[1], name: httpsMatch[2] };
25720
26056
  const sshMatch = remoteUrl.match(/github\.com:([^\/]+)\/([^\/]+?)(?:\.git)?$/);
@@ -25725,7 +26061,7 @@ function detectGitHubRepo(cachedRepo) {
25725
26061
  }
25726
26062
  function detectGitBranch() {
25727
26063
  try {
25728
- return execSync4("git branch --show-current", { cwd: process.cwd(), encoding: "utf8" }).trim() || null;
26064
+ return execSync5("git branch --show-current", { cwd: process.cwd(), encoding: "utf8" }).trim() || null;
25729
26065
  } catch {
25730
26066
  return null;
25731
26067
  }
@@ -26165,7 +26501,7 @@ var init_multi_agent_modal = __esm({
26165
26501
  });
26166
26502
 
26167
26503
  // src/hooks/recommended.ts
26168
- import { platform as platform6 } from "os";
26504
+ import { platform as platform7 } from "os";
26169
26505
  function getRecommendedHook(id) {
26170
26506
  return RECOMMENDED_HOOKS.find((r) => r.id === id);
26171
26507
  }
@@ -26173,7 +26509,7 @@ var isMac, RECOMMENDED_HOOKS;
26173
26509
  var init_recommended = __esm({
26174
26510
  "src/hooks/recommended.ts"() {
26175
26511
  "use strict";
26176
- isMac = platform6() === "darwin";
26512
+ isMac = platform7() === "darwin";
26177
26513
  RECOMMENDED_HOOKS = [
26178
26514
  // ── Stop notifications ───────────────────────────────────────────────
26179
26515
  {
@@ -27068,6 +27404,7 @@ var init_help_menu = __esm({
27068
27404
  { command: "/cost", description: "show cost report" },
27069
27405
  { command: "/model", description: "show current model" },
27070
27406
  { command: "/update", description: "check for updates" },
27407
+ { command: "/update camouflage", description: "check for camouflage-tui updates" },
27071
27408
  { command: "/hello", description: "send a voice note to the creator" }
27072
27409
  ]
27073
27410
  },
@@ -27428,6 +27765,38 @@ var init_modal_host = __esm({
27428
27765
  }
27429
27766
  });
27430
27767
 
27768
+ // src/ui/plan-complete-picker.tsx
27769
+ import { Box as Box39, Text as Text40 } from "ink";
27770
+ import SelectInput22 from "ink-select-input";
27771
+ import { jsx as jsx41, jsxs as jsxs39 } from "react/jsx-runtime";
27772
+ function PlanCompletePicker({ onPick }) {
27773
+ const theme = useTheme();
27774
+ const items = [
27775
+ { label: "\u25B8 Execute this plan and accept changes (auto mode)", value: "auto", key: "auto" },
27776
+ { label: "\u25B8 Start building and ask for permission (edit mode)", value: "edit", key: "edit" },
27777
+ { label: "\u25B8 Continue planning / ask a question", value: "continue", key: "continue" }
27778
+ ];
27779
+ return /* @__PURE__ */ jsxs39(Box39, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
27780
+ /* @__PURE__ */ jsx41(Text40, { color: theme.accent, bold: true, children: "Plan complete \u2014 what next?" }),
27781
+ /* @__PURE__ */ jsx41(Text40, { color: theme.info.color, dimColor: false, children: "Arrow keys to navigate, Enter to select, Esc to cancel." }),
27782
+ /* @__PURE__ */ jsx41(Box39, { marginTop: 1, children: /* @__PURE__ */ jsx41(
27783
+ SelectInput22,
27784
+ {
27785
+ items,
27786
+ onSelect: (item) => onPick(item.value),
27787
+ onHighlight: () => {
27788
+ }
27789
+ }
27790
+ ) })
27791
+ ] });
27792
+ }
27793
+ var init_plan_complete_picker = __esm({
27794
+ "src/ui/plan-complete-picker.tsx"() {
27795
+ "use strict";
27796
+ init_theme_context();
27797
+ }
27798
+ });
27799
+
27431
27800
  // src/ui/use-session-manager.ts
27432
27801
  import { useCallback as useCallback8, useRef as useRef5, useState as useState25 } from "react";
27433
27802
  function extractFirstUserText(messages) {
@@ -27764,64 +28133,6 @@ var init_input_handlers = __esm({
27764
28133
  }
27765
28134
  });
27766
28135
 
27767
- // src/agent/distill.ts
27768
- function distillSessionPlan(messages) {
27769
- for (let i = messages.length - 1; i >= 0; i--) {
27770
- const m = messages[i];
27771
- if (m?.role !== "assistant") continue;
27772
- let text = "";
27773
- if (typeof m.content === "string") {
27774
- text = m.content;
27775
- } else if (Array.isArray(m.content)) {
27776
- text = m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
27777
- }
27778
- text = text.trim();
27779
- if (text.length > 20) {
27780
- return text;
27781
- }
27782
- }
27783
- return null;
27784
- }
27785
- var init_distill = __esm({
27786
- "src/agent/distill.ts"() {
27787
- "use strict";
27788
- }
27789
- });
27790
-
27791
- // src/util/clipboard.ts
27792
- import { execSync as execSync5 } from "child_process";
27793
- import { platform as platform7 } from "os";
27794
- function writeToClipboard(text) {
27795
- const os2 = platform7();
27796
- try {
27797
- if (os2 === "darwin") {
27798
- execSync5("pbcopy", { input: text, timeout: 5e3 });
27799
- return { success: true, message: "Copied to clipboard" };
27800
- }
27801
- if (os2 === "win32") {
27802
- execSync5("clip", { input: text, timeout: 5e3 });
27803
- return { success: true, message: "Copied to clipboard" };
27804
- }
27805
- try {
27806
- execSync5("xclip -selection clipboard", { input: text, timeout: 5e3 });
27807
- return { success: true, message: "Copied to clipboard" };
27808
- } catch {
27809
- execSync5("xsel --clipboard --input", { input: text, timeout: 5e3 });
27810
- return { success: true, message: "Copied to clipboard" };
27811
- }
27812
- } catch {
27813
- return {
27814
- success: false,
27815
- message: "Clipboard not available \u2014 plan will be shown below"
27816
- };
27817
- }
27818
- }
27819
- var init_clipboard = __esm({
27820
- "src/util/clipboard.ts"() {
27821
- "use strict";
27822
- }
27823
- });
27824
-
27825
28136
  // src/cost-attribution/tui-report.ts
27826
28137
  var tui_report_exports = {};
27827
28138
  __export(tui_report_exports, {
@@ -27905,7 +28216,7 @@ var init_tui_report = __esm({
27905
28216
 
27906
28217
  // src/ui/slash-commands.ts
27907
28218
  import { join as join32 } from "path";
27908
- import { unlink as unlink4 } from "fs/promises";
28219
+ import { unlink as unlink5 } from "fs/promises";
27909
28220
  import QRCode from "qrcode";
27910
28221
  function dispatchSlashCommand(ctx, cmd) {
27911
28222
  const raw = cmd.trim();
@@ -28771,8 +29082,27 @@ ${lines.join("\n")}` }]);
28771
29082
  void ctx.runInit();
28772
29083
  return true;
28773
29084
  };
28774
- handleUpdate = (ctx) => {
29085
+ handleUpdate = (ctx, _rest, arg) => {
28775
29086
  const { setEvents, mkKey: mkKey2 } = ctx;
29087
+ if (arg === "camouflage") {
29088
+ void checkOptionalDependency("camouflage-tui", "beta").then((dep) => {
29089
+ if (dep.hasUpdate && dep.latestVersion) {
29090
+ setEvents((e) => [
29091
+ ...e,
29092
+ { kind: "info", key: mkKey2(), text: `camouflage-tui update available: ${dep.localVersion} \u2192 ${dep.latestVersion}` }
29093
+ ]);
29094
+ setEvents((e) => [
29095
+ ...e,
29096
+ { kind: "info", key: mkKey2(), text: "run: npm update camouflage-tui" }
29097
+ ]);
29098
+ } else if (dep.localVersion) {
29099
+ setEvents((e) => [...e, { kind: "info", key: mkKey2(), text: `camouflage-tui up to date (${dep.localVersion})` }]);
29100
+ } else {
29101
+ setEvents((e) => [...e, { kind: "info", key: mkKey2(), text: "camouflage-tui is not installed" }]);
29102
+ }
29103
+ });
29104
+ return true;
29105
+ }
28776
29106
  void checkForUpdate(true).then((result) => {
28777
29107
  if (result.hasUpdate) {
28778
29108
  ctx.setHasUpdate(true);
@@ -29027,7 +29357,7 @@ project: ${projectSettingsPath(cwd)}`
29027
29357
  return true;
29028
29358
  };
29029
29359
  handleLogout = (ctx) => {
29030
- unlink4(configPath()).catch(() => {
29360
+ unlink5(configPath()).catch(() => {
29031
29361
  });
29032
29362
  ctx.setEvents((e) => [
29033
29363
  ...e,
@@ -30397,10 +30727,10 @@ __export(app_exports, {
30397
30727
  renderApp: () => renderApp
30398
30728
  });
30399
30729
  import React23, { useState as useState27, useRef as useRef7, useEffect as useEffect11, useCallback as useCallback10, useMemo as useMemo6 } from "react";
30400
- import { Box as Box39, Text as Text40, useApp, useInput as useInput19, render } from "ink";
30730
+ import { Box as Box40, Text as Text41, useApp, useInput as useInput19, render } from "ink";
30401
30731
  import { existsSync as existsSync8 } from "fs";
30402
30732
  import { join as join37 } from "path";
30403
- import { jsx as jsx41, jsxs as jsxs39 } from "react/jsx-runtime";
30733
+ import { jsx as jsx42, jsxs as jsxs40 } from "react/jsx-runtime";
30404
30734
  function App({
30405
30735
  initialCfg,
30406
30736
  initialUpdateResult,
@@ -30530,6 +30860,8 @@ function App({
30530
30860
  setShowSkillsPicker,
30531
30861
  showShellPicker,
30532
30862
  setShowShellPicker,
30863
+ showPlanCompletePicker,
30864
+ setShowPlanCompletePicker,
30533
30865
  hasFullscreenModal,
30534
30866
  hasAnyModal
30535
30867
  } = modals;
@@ -30823,6 +31155,26 @@ ${wcagWarnings.join("\n")}` }
30823
31155
  ]);
30824
31156
  }
30825
31157
  });
31158
+ void checkOptionalDependency("camouflage-tui", "beta").then((dep) => {
31159
+ if (dep.hasUpdate && dep.latestVersion) {
31160
+ setEvents((e) => [
31161
+ ...e,
31162
+ {
31163
+ kind: "info",
31164
+ key: mkKey(),
31165
+ text: `camouflage-tui update available: ${dep.localVersion} \u2192 ${dep.latestVersion}`
31166
+ }
31167
+ ]);
31168
+ setEvents((e) => [
31169
+ ...e,
31170
+ {
31171
+ kind: "info",
31172
+ key: mkKey(),
31173
+ text: "run: npm update camouflage-tui"
31174
+ }
31175
+ ]);
31176
+ }
31177
+ });
30826
31178
  }, [cfg, initialUpdateResult]);
30827
31179
  useEffect11(() => {
30828
31180
  modeRef.current = mode;
@@ -30882,6 +31234,26 @@ ${wcagWarnings.join("\n")}` }
30882
31234
  }
30883
31235
  }
30884
31236
  });
31237
+ void checkOptionalDependency("camouflage-tui", "beta").then((dep) => {
31238
+ if (dep.hasUpdate && dep.latestVersion) {
31239
+ setEvents((e) => [
31240
+ ...e,
31241
+ {
31242
+ kind: "info",
31243
+ key: mkKey(),
31244
+ text: `camouflage-tui update available: ${dep.localVersion} \u2192 ${dep.latestVersion}`
31245
+ }
31246
+ ]);
31247
+ setEvents((e) => [
31248
+ ...e,
31249
+ {
31250
+ kind: "info",
31251
+ key: mkKey(),
31252
+ text: "run: npm update camouflage-tui"
31253
+ }
31254
+ ]);
31255
+ }
31256
+ });
30885
31257
  }, 30 * 60 * 1e3);
30886
31258
  return () => clearInterval(id);
30887
31259
  }, [cfg]);
@@ -31287,6 +31659,61 @@ ${wcagWarnings.join("\n")}` }
31287
31659
  },
31288
31660
  [mkKey, setShowUiPicker]
31289
31661
  );
31662
+ const handlePlanCompletePick = useCallback10(
31663
+ (picked) => {
31664
+ setShowPlanCompletePicker(false);
31665
+ if (!picked || picked === "continue") return;
31666
+ const plan = distillSessionPlan(messagesRef.current);
31667
+ if (!plan) {
31668
+ setEvents((e) => [
31669
+ ...e,
31670
+ { kind: "error", key: mkKey(), text: "No plan found to start fresh with." }
31671
+ ]);
31672
+ setMode(picked);
31673
+ return;
31674
+ }
31675
+ const clipResult = writeToClipboard(plan);
31676
+ if (cacheStableRef.current && messagesRef.current.length >= 2) {
31677
+ messagesRef.current = [messagesRef.current[0], messagesRef.current[1]];
31678
+ } else {
31679
+ messagesRef.current = [messagesRef.current[0]];
31680
+ }
31681
+ resetSession();
31682
+ executorRef.current.clearArtifacts();
31683
+ if (flushTimeoutRef.current) {
31684
+ clearTimeout(flushTimeoutRef.current);
31685
+ flushTimeoutRef.current = null;
31686
+ }
31687
+ pendingTextRef.current.clear();
31688
+ activeAsstIdRef.current = null;
31689
+ pendingToolCallsRef.current.clear();
31690
+ usageRef.current = null;
31691
+ turnCounterRef.current = 0;
31692
+ setEvents([]);
31693
+ setUsage(null);
31694
+ setSessionUsage(null);
31695
+ gatewayMetaRef.current = null;
31696
+ setGatewayMeta(null);
31697
+ clearTaskTracking();
31698
+ compactSuggestedRef.current = false;
31699
+ updateNudgedRef.current = false;
31700
+ setEvents((e) => [
31701
+ ...e,
31702
+ {
31703
+ kind: "info",
31704
+ key: mkKey(),
31705
+ text: clipResult.success ? `Plan copied to clipboard. Starting fresh session in ${picked} mode with plan only\u2026` : `Clipboard unavailable. Starting fresh session in ${picked} mode with plan only\u2026`
31706
+ }
31707
+ ]);
31708
+ if (!clipResult.success) {
31709
+ setEvents((e) => [...e, { kind: "info", key: mkKey(), text: "--- Plan ---\n" + plan }]);
31710
+ }
31711
+ setMode(picked);
31712
+ modeRef.current = picked;
31713
+ submitRef.current(plan);
31714
+ },
31715
+ [mkKey, setShowPlanCompletePicker, setMode, setEvents, setUsage, setSessionUsage, setGatewayMeta, clearTaskTracking, resetSession, submitRef]
31716
+ );
31290
31717
  const handleModelPick = useCallback10(
31291
31718
  (picked) => {
31292
31719
  setShowModelPicker(false);
@@ -32149,6 +32576,12 @@ ${conflicts.join("\n")}` }
32149
32576
  }
32150
32577
  })();
32151
32578
  cleanupTurn();
32579
+ if (modeRef.current === "plan") {
32580
+ const plan = distillSessionPlan(messagesRef.current);
32581
+ if (plan) {
32582
+ setShowPlanCompletePicker(true);
32583
+ }
32584
+ }
32152
32585
  },
32153
32586
  onError: async (e) => {
32154
32587
  if (e.name === "AbortError") {
@@ -32182,7 +32615,7 @@ ${conflicts.join("\n")}` }
32182
32615
  }
32183
32616
  );
32184
32617
  },
32185
- [cfg, handleSlash, updateAssistant, updateTool, saveSessionSafe, updateGatewayMeta]
32618
+ [cfg, handleSlash, updateAssistant, updateTool, saveSessionSafe, updateGatewayMeta, setShowPlanCompletePicker]
32186
32619
  );
32187
32620
  useEffect11(() => {
32188
32621
  if (!busy && queue2.length > 0 && supervisorRef.current.phase === "idle") {
@@ -32243,7 +32676,7 @@ ${conflicts.join("\n")}` }
32243
32676
  });
32244
32677
  }, [usage, modelContextLimit, busy, runCompact2]);
32245
32678
  if (!cfg) {
32246
- return /* @__PURE__ */ jsx41(ThemeProvider, { theme, children: /* @__PURE__ */ jsx41(
32679
+ return /* @__PURE__ */ jsx42(ThemeProvider, { theme, children: /* @__PURE__ */ jsx42(
32247
32680
  Onboarding,
32248
32681
  {
32249
32682
  onCancel: () => exit(),
@@ -32258,7 +32691,7 @@ ${conflicts.join("\n")}` }
32258
32691
  ) });
32259
32692
  }
32260
32693
  if (checkpointSession !== null) {
32261
- return /* @__PURE__ */ jsx41(ThemeProvider, { theme, children: /* @__PURE__ */ jsx41(Box39, { flexDirection: "column", children: /* @__PURE__ */ jsx41(
32694
+ return /* @__PURE__ */ jsx42(ThemeProvider, { theme, children: /* @__PURE__ */ jsx42(Box40, { flexDirection: "column", children: /* @__PURE__ */ jsx42(
32262
32695
  CheckpointPicker,
32263
32696
  {
32264
32697
  session: checkpointSession,
@@ -32268,10 +32701,10 @@ ${conflicts.join("\n")}` }
32268
32701
  ) }) });
32269
32702
  }
32270
32703
  if (resumeSessions !== null) {
32271
- return /* @__PURE__ */ jsx41(ThemeProvider, { theme, children: /* @__PURE__ */ jsx41(Box39, { flexDirection: "column", children: /* @__PURE__ */ jsx41(ResumePicker, { sessions: resumeSessions, onPick: handleResumePick }) }) });
32704
+ return /* @__PURE__ */ jsx42(ThemeProvider, { theme, children: /* @__PURE__ */ jsx42(Box40, { flexDirection: "column", children: /* @__PURE__ */ jsx42(ResumePicker, { sessions: resumeSessions, onPick: handleResumePick }) }) });
32272
32705
  }
32273
32706
  if (hasFullscreenModal) {
32274
- return /* @__PURE__ */ jsx41(
32707
+ return /* @__PURE__ */ jsx42(
32275
32708
  ModalHost,
32276
32709
  {
32277
32710
  modals,
@@ -32415,9 +32848,9 @@ ${conflicts.join("\n")}` }
32415
32848
  );
32416
32849
  }
32417
32850
  const hasConversation = events.some((e) => e.kind === "user" || e.kind === "assistant");
32418
- return /* @__PURE__ */ jsx41(ThemeProvider, { theme, children: /* @__PURE__ */ jsxs39(Box39, { flexDirection: "column", children: [
32419
- !hasConversation && events.length === 0 ? /* @__PURE__ */ jsx41(Welcome, {}) : /* @__PURE__ */ jsx41(ChatView, { events, showReasoning, verbose, intentTier: intentTier ?? void 0 }),
32420
- perm ? /* @__PURE__ */ jsx41(
32851
+ return /* @__PURE__ */ jsx42(ThemeProvider, { theme, children: /* @__PURE__ */ jsxs40(Box40, { flexDirection: "column", children: [
32852
+ !hasConversation && events.length === 0 ? /* @__PURE__ */ jsx42(Welcome, {}) : /* @__PURE__ */ jsx42(ChatView, { events, showReasoning, verbose, intentTier: intentTier ?? void 0 }),
32853
+ perm ? /* @__PURE__ */ jsx42(
32421
32854
  PermissionModal,
32422
32855
  {
32423
32856
  tool: perm.tool,
@@ -32427,7 +32860,7 @@ ${conflicts.join("\n")}` }
32427
32860
  submitRef.current(text);
32428
32861
  }
32429
32862
  }
32430
- ) : limitModal || loopModal ? /* @__PURE__ */ jsx41(
32863
+ ) : limitModal || loopModal ? /* @__PURE__ */ jsx42(
32431
32864
  ModalOverlay,
32432
32865
  {
32433
32866
  modals,
@@ -32438,9 +32871,9 @@ ${conflicts.join("\n")}` }
32438
32871
  loopResolveRef.current = null;
32439
32872
  }
32440
32873
  }
32441
- ) : /* @__PURE__ */ jsxs39(Box39, { flexDirection: "column", marginTop: 1, children: [
32442
- (activeWorkers.length > 0 || coordinatorNarration) && /* @__PURE__ */ jsx41(WorkerList, { workers: activeWorkers, isSynthesizing, narration: coordinatorNarration }),
32443
- tasks.length > 0 && /* @__PURE__ */ jsx41(
32874
+ ) : showPlanCompletePicker ? /* @__PURE__ */ jsx42(PlanCompletePicker, { onPick: handlePlanCompletePick }) : /* @__PURE__ */ jsxs40(Box40, { flexDirection: "column", marginTop: 1, children: [
32875
+ (activeWorkers.length > 0 || coordinatorNarration) && /* @__PURE__ */ jsx42(WorkerList, { workers: activeWorkers, isSynthesizing, narration: coordinatorNarration }),
32876
+ tasks.length > 0 && /* @__PURE__ */ jsx42(
32444
32877
  TaskList,
32445
32878
  {
32446
32879
  tasks,
@@ -32448,11 +32881,11 @@ ${conflicts.join("\n")}` }
32448
32881
  tokensDelta: Math.max(0, (usage?.prompt_tokens ?? 0) - tasksStartTokens)
32449
32882
  }
32450
32883
  ),
32451
- queue2.length > 0 && /* @__PURE__ */ jsx41(Box39, { flexDirection: "column", marginBottom: 1, children: queue2.map((q, i) => /* @__PURE__ */ jsxs39(Text40, { color: theme.info.color, dimColor: theme.info.dim, children: [
32884
+ queue2.length > 0 && /* @__PURE__ */ jsx42(Box40, { flexDirection: "column", marginBottom: 1, children: queue2.map((q, i) => /* @__PURE__ */ jsxs40(Text41, { color: theme.info.color, dimColor: theme.info.dim, children: [
32452
32885
  "\u23F3 ",
32453
32886
  q.display
32454
32887
  ] }, `queue_${i}`)) }),
32455
- /* @__PURE__ */ jsx41(
32888
+ /* @__PURE__ */ jsx42(
32456
32889
  StatusBar,
32457
32890
  {
32458
32891
  usage,
@@ -32474,7 +32907,7 @@ ${conflicts.join("\n")}` }
32474
32907
  intentTier: intentTier ?? void 0
32475
32908
  }
32476
32909
  ),
32477
- picker.active?.kind === "file" && /* @__PURE__ */ jsx41(
32910
+ picker.active?.kind === "file" && /* @__PURE__ */ jsx42(
32478
32911
  FilePicker,
32479
32912
  {
32480
32913
  items: picker.fileItems,
@@ -32483,7 +32916,7 @@ ${conflicts.join("\n")}` }
32483
32916
  recentFiles: new Set(recentFilesRef.current.keys())
32484
32917
  }
32485
32918
  ),
32486
- picker.active?.kind === "slash" && /* @__PURE__ */ jsx41(
32919
+ picker.active?.kind === "slash" && /* @__PURE__ */ jsx42(
32487
32920
  SlashPicker,
32488
32921
  {
32489
32922
  items: picker.slashItems,
@@ -32491,9 +32924,9 @@ ${conflicts.join("\n")}` }
32491
32924
  query: picker.query
32492
32925
  }
32493
32926
  ),
32494
- /* @__PURE__ */ jsxs39(Box39, { marginTop: 1, children: [
32495
- /* @__PURE__ */ jsx41(Text40, { color: theme.prompt ?? theme.accent, children: "\u203A " }),
32496
- /* @__PURE__ */ jsx41(
32927
+ /* @__PURE__ */ jsxs40(Box40, { marginTop: 1, children: [
32928
+ /* @__PURE__ */ jsx42(Text41, { color: theme.prompt ?? theme.accent, children: "\u203A " }),
32929
+ /* @__PURE__ */ jsx42(
32497
32930
  CustomTextInput,
32498
32931
  {
32499
32932
  value: input,
@@ -32550,7 +32983,7 @@ ${conflicts.join("\n")}` }
32550
32983
  }
32551
32984
  async function renderApp(cfg, updateResult, lspScope = "global", lspProjectPath = null) {
32552
32985
  const instance = render(
32553
- /* @__PURE__ */ jsx41(
32986
+ /* @__PURE__ */ jsx42(
32554
32987
  App,
32555
32988
  {
32556
32989
  initialCfg: cfg,
@@ -32623,6 +33056,7 @@ var init_app = __esm({
32623
33056
  init_use_picker_controller();
32624
33057
  init_use_modal_host();
32625
33058
  init_modal_host();
33059
+ init_plan_complete_picker();
32626
33060
  init_use_session_manager();
32627
33061
  init_use_turn_controller();
32628
33062
  init_input_handlers();
@@ -32631,6 +33065,8 @@ var init_app = __esm({
32631
33065
  init_run_startup_tasks();
32632
33066
  init_manager_init();
32633
33067
  init_run_compact();
33068
+ init_distill();
33069
+ init_clipboard();
32634
33070
  init_command_handlers();
32635
33071
  init_app_helpers();
32636
33072
  }