zidane 5.14.5 → 6.0.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.
Files changed (198) hide show
  1. package/dist/{acp-BpFEbCMQ.js → acp-Irby04Us.js} +7 -8
  2. package/dist/{acp-BpFEbCMQ.js.map → acp-Irby04Us.js.map} +1 -1
  3. package/dist/acp-cli.js +7 -7
  4. package/dist/acp.d.ts +2 -3
  5. package/dist/acp.d.ts.map +1 -1
  6. package/dist/acp.js +1 -1
  7. package/dist/{agent-DyI0YsAR.js → agent-DkSmGkDM.js} +10873 -5437
  8. package/dist/agent-DkSmGkDM.js.map +1 -0
  9. package/dist/agent.d.ts +1 -2
  10. package/dist/agent.js +2 -2
  11. package/dist/{anthropic-cbfW2RGg.js → anthropic-CTHsdFG9.js} +5 -4
  12. package/dist/{anthropic-cbfW2RGg.js.map → anthropic-CTHsdFG9.js.map} +1 -1
  13. package/dist/{auth-D9uj0nNr.js → auth-BDSu_0t3.js} +13 -86
  14. package/dist/auth-BDSu_0t3.js.map +1 -0
  15. package/dist/chat/pure.d.ts +4 -4
  16. package/dist/chat/pure.js +4 -3
  17. package/dist/chat.d.ts +29 -8
  18. package/dist/chat.d.ts.map +1 -1
  19. package/dist/chat.js +9 -6
  20. package/dist/chat.js.map +1 -1
  21. package/dist/completion-core-CXYSVhZo.js +147 -0
  22. package/dist/completion-core-CXYSVhZo.js.map +1 -0
  23. package/dist/{context-breakdown-kO-pDsay.js → context-breakdown-Dzo25N45.js} +2 -23
  24. package/dist/context-breakdown-Dzo25N45.js.map +1 -0
  25. package/dist/contexts/daytona.d.ts +3 -3
  26. package/dist/contexts/daytona.js +1 -1
  27. package/dist/contexts/docker.d.ts +10 -2
  28. package/dist/contexts/docker.d.ts.map +1 -1
  29. package/dist/contexts/docker.js +142 -103
  30. package/dist/contexts/docker.js.map +1 -1
  31. package/dist/contexts/e2b.d.ts +2 -2
  32. package/dist/contexts/e2b.js +1 -1
  33. package/dist/contexts/sandbox.d.ts +1 -1
  34. package/dist/contexts/sandbox.js +4 -1
  35. package/dist/contexts/sandbox.js.map +1 -1
  36. package/dist/{contexts-Lzf5huID.js → contexts-Vone3qQQ.js} +26 -37
  37. package/dist/contexts-Vone3qQQ.js.map +1 -0
  38. package/dist/contexts.d.ts +3 -3
  39. package/dist/contexts.js +1 -1
  40. package/dist/errors.d.ts +1 -2
  41. package/dist/errors.js +1 -1
  42. package/dist/eval.d.ts +1 -1
  43. package/dist/eval.js +3 -3
  44. package/dist/exec-file-size-PXkr-MGA.js +63 -0
  45. package/dist/exec-file-size-PXkr-MGA.js.map +1 -0
  46. package/dist/extensions.d.ts +2 -0
  47. package/dist/extensions.js +2 -0
  48. package/dist/generate-data-WL_yGVL6.js +324 -0
  49. package/dist/generate-data-WL_yGVL6.js.map +1 -0
  50. package/dist/headless.d.ts +1 -1
  51. package/dist/headless.js +15 -10
  52. package/dist/headless.js.map +1 -1
  53. package/dist/index-B8UtiZy7.d.ts +14967 -0
  54. package/dist/index-B8UtiZy7.d.ts.map +1 -0
  55. package/dist/{index-B3ciFUZx.d.ts → index-DsPQbyUn.d.ts} +2 -2
  56. package/dist/index-DsPQbyUn.d.ts.map +1 -0
  57. package/dist/index.d.ts +6 -12
  58. package/dist/index.js +20 -43
  59. package/dist/index.js.map +1 -1
  60. package/dist/{logger-Ktm-lj1s.js → logger-DBX9uYOw.js} +108 -5
  61. package/dist/logger-DBX9uYOw.js.map +1 -0
  62. package/dist/login-DOF8pMVM.js +539 -0
  63. package/dist/login-DOF8pMVM.js.map +1 -0
  64. package/dist/{mcp-Dn5W65Lv.js → mcp-CcvuVXB9.js} +2 -2
  65. package/dist/{mcp-Dn5W65Lv.js.map → mcp-CcvuVXB9.js.map} +1 -1
  66. package/dist/mcp.d.ts +1 -1
  67. package/dist/mcp.js +1 -1
  68. package/dist/{messages-CozesxbB.js → messages-CwujiS74.js} +4 -4
  69. package/dist/messages-CwujiS74.js.map +1 -0
  70. package/dist/{openai-compat-FII_9wJP.js → openai-compat-BvgYGSR1.js} +5 -4
  71. package/dist/{openai-compat-FII_9wJP.js.map → openai-compat-BvgYGSR1.js.map} +1 -1
  72. package/dist/output/stream-json.d.ts +1 -2
  73. package/dist/output/stream-json.d.ts.map +1 -1
  74. package/dist/output/terminal.d.ts +1 -2
  75. package/dist/output/terminal.d.ts.map +1 -1
  76. package/dist/{glob-DluQFSw1.js → output-cap-kBUHBlnq.js} +50 -2
  77. package/dist/output-cap-kBUHBlnq.js.map +1 -0
  78. package/dist/presets.d.ts +1 -2
  79. package/dist/presets.js +1 -1
  80. package/dist/providers/anthropic.d.ts +1 -1
  81. package/dist/providers/anthropic.js +1 -1
  82. package/dist/providers/arcee.d.ts +1 -1
  83. package/dist/providers/arcee.js +1 -1
  84. package/dist/providers/baseten.d.ts +1 -1
  85. package/dist/providers/baseten.js +1 -1
  86. package/dist/providers/cerebras.d.ts +1 -1
  87. package/dist/providers/cerebras.js +1 -1
  88. package/dist/providers/local.d.ts +1 -1
  89. package/dist/providers/local.js +1 -1
  90. package/dist/providers/openai-compat.d.ts +1 -1
  91. package/dist/providers/openai-compat.js +1 -1
  92. package/dist/providers/openai.d.ts +1 -1
  93. package/dist/providers/openai.js +3 -3
  94. package/dist/providers/openrouter.d.ts +1 -1
  95. package/dist/providers/openrouter.js +1 -1
  96. package/dist/providers/xai.d.ts +1 -1
  97. package/dist/providers/xai.js +1 -1
  98. package/dist/{providers-DYXj2LcE.js → providers-z6LDMAr3.js} +3 -3
  99. package/dist/{providers-DYXj2LcE.js.map → providers-z6LDMAr3.js.map} +1 -1
  100. package/dist/providers.d.ts +1 -1
  101. package/dist/providers.js +4 -4
  102. package/dist/{read-state-BymF41Jb.js → read-state-Dq_TXUX1.js} +2 -2
  103. package/dist/{read-state-BymF41Jb.js.map → read-state-Dq_TXUX1.js.map} +1 -1
  104. package/dist/{interpolate-BtIgcCuz.js → resolve-Cr8JSL_O.js} +562 -136
  105. package/dist/resolve-Cr8JSL_O.js.map +1 -0
  106. package/dist/restate.d.ts +2 -2
  107. package/dist/restate.js +2 -2
  108. package/dist/{sandbox-Dgy-m6UF.d.ts → sandbox-CgjG2VvQ.d.ts} +2 -2
  109. package/dist/{sandbox-Dgy-m6UF.d.ts.map → sandbox-CgjG2VvQ.d.ts.map} +1 -1
  110. package/dist/session/sqlite.d.ts +1 -1
  111. package/dist/{session-BRCsZsXa.js → session-CsdDIPY8.js} +108 -3
  112. package/dist/session-CsdDIPY8.js.map +1 -0
  113. package/dist/session.d.ts +2 -2
  114. package/dist/session.js +3 -3
  115. package/dist/skills-Bsb0rvWI.js +295 -0
  116. package/dist/skills-Bsb0rvWI.js.map +1 -0
  117. package/dist/skills.d.ts +2 -3
  118. package/dist/skills.js +3 -24
  119. package/dist/{errors-CkTvg97v.d.ts → timeout-8vSIRjzl.d.ts} +10 -2
  120. package/dist/timeout-8vSIRjzl.d.ts.map +1 -0
  121. package/dist/{timeout-kGGSXagK.js → timeout-BDetbuN6.js} +59 -2
  122. package/dist/timeout-BDetbuN6.js.map +1 -0
  123. package/dist/tool-formatters-DY0TDb1r.d.ts +378 -0
  124. package/dist/tool-formatters-DY0TDb1r.d.ts.map +1 -0
  125. package/dist/tools/fetch-url.d.ts +1 -1
  126. package/dist/tools/web-search.d.ts +1 -1
  127. package/dist/tools.d.ts +2 -3
  128. package/dist/tools.js +3 -4
  129. package/dist/{transcript-anchors-BEKMYhfo.d.ts → transcript-anchors-Cb-bDbNH.d.ts} +231 -638
  130. package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +1 -0
  131. package/dist/{transcript-anchors-DR4URysA.js → transcript-anchors-DSRPWjiH.js} +189 -984
  132. package/dist/transcript-anchors-DSRPWjiH.js.map +1 -0
  133. package/dist/tui.d.ts +329 -11
  134. package/dist/tui.d.ts.map +1 -1
  135. package/dist/tui.js +5367 -1418
  136. package/dist/tui.js.map +1 -1
  137. package/dist/{turn-operations-BhglWus_.d.ts → turn-operations-CgMROLsr.d.ts} +29 -4
  138. package/dist/turn-operations-CgMROLsr.d.ts.map +1 -0
  139. package/dist/{turn-operations-tG0vuBNc.js → turn-operations-Dq9Kx7m2.js} +321 -81
  140. package/dist/turn-operations-Dq9Kx7m2.js.map +1 -0
  141. package/dist/{types-BqdTeBaC.d.ts → types-BDccavwj.d.ts} +42 -1
  142. package/dist/types-BDccavwj.d.ts.map +1 -0
  143. package/dist/{types-CyVGdbia.js → types-BIarq1qE.js} +90 -2
  144. package/dist/types-BIarq1qE.js.map +1 -0
  145. package/dist/types.d.ts +5 -6
  146. package/dist/types.js +1 -1
  147. package/dist/{xai-D9tfkrIe.js → xai-B_e6TOA8.js} +2 -2
  148. package/dist/{xai-D9tfkrIe.js.map → xai-B_e6TOA8.js.map} +1 -1
  149. package/docs/EXECUTION_CONTEXT.md +16 -0
  150. package/docs/EXTENSIONS.md +1063 -0
  151. package/docs/SKILL.md +16 -0
  152. package/docs/TUI.md +4 -0
  153. package/package.json +12 -3
  154. package/dist/agent-DCtsviti.d.ts +0 -6955
  155. package/dist/agent-DCtsviti.d.ts.map +0 -1
  156. package/dist/agent-DyI0YsAR.js.map +0 -1
  157. package/dist/auth-D9uj0nNr.js.map +0 -1
  158. package/dist/context-breakdown-kO-pDsay.js.map +0 -1
  159. package/dist/contexts-Lzf5huID.js.map +0 -1
  160. package/dist/errors-CkTvg97v.d.ts.map +0 -1
  161. package/dist/glob-DluQFSw1.js.map +0 -1
  162. package/dist/glob-shell-rJMoCIGb.js +0 -21
  163. package/dist/glob-shell-rJMoCIGb.js.map +0 -1
  164. package/dist/index-B3ciFUZx.d.ts.map +0 -1
  165. package/dist/index-CIbemeZd.d.ts +0 -316
  166. package/dist/index-CIbemeZd.d.ts.map +0 -1
  167. package/dist/index-DsIW6pmv.d.ts +0 -2738
  168. package/dist/index-DsIW6pmv.d.ts.map +0 -1
  169. package/dist/interpolate-BtIgcCuz.js.map +0 -1
  170. package/dist/logger-Ktm-lj1s.js.map +0 -1
  171. package/dist/logger-mw-311EC.d.ts +0 -102
  172. package/dist/logger-mw-311EC.d.ts.map +0 -1
  173. package/dist/login-CCA-1lgK.js +0 -267
  174. package/dist/login-CCA-1lgK.js.map +0 -1
  175. package/dist/messages-CozesxbB.js.map +0 -1
  176. package/dist/policy-DcGlpaNs.d.ts +0 -129
  177. package/dist/policy-DcGlpaNs.d.ts.map +0 -1
  178. package/dist/presets-DtyUA1-S.js +0 -114
  179. package/dist/presets-DtyUA1-S.js.map +0 -1
  180. package/dist/run-summary-BExU5Flb.d.ts +0 -249
  181. package/dist/run-summary-BExU5Flb.d.ts.map +0 -1
  182. package/dist/session-BRCsZsXa.js.map +0 -1
  183. package/dist/shell-quote-BmnhZmdM.js +0 -33
  184. package/dist/shell-quote-BmnhZmdM.js.map +0 -1
  185. package/dist/skills.js.map +0 -1
  186. package/dist/timeout-CpFm0jJ6.d.ts +0 -10
  187. package/dist/timeout-CpFm0jJ6.d.ts.map +0 -1
  188. package/dist/timeout-kGGSXagK.js.map +0 -1
  189. package/dist/tool-formatters-CpCEWwKt.d.ts +0 -1525
  190. package/dist/tool-formatters-CpCEWwKt.d.ts.map +0 -1
  191. package/dist/tools-NSzbC6Nr.js +0 -1739
  192. package/dist/tools-NSzbC6Nr.js.map +0 -1
  193. package/dist/transcript-anchors-BEKMYhfo.d.ts.map +0 -1
  194. package/dist/transcript-anchors-DR4URysA.js.map +0 -1
  195. package/dist/turn-operations-BhglWus_.d.ts.map +0 -1
  196. package/dist/turn-operations-tG0vuBNc.js.map +0 -1
  197. package/dist/types-BqdTeBaC.d.ts.map +0 -1
  198. package/dist/types-CyVGdbia.js.map +0 -1
@@ -1,26 +1,25 @@
1
- import { s as toolResultToText } from "./types-CyVGdbia.js";
2
- import { u as shell } from "./agent-DyI0YsAR.js";
3
- import { a as multiEdit, c as grep, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, t as writeFile$1, u as edit } from "./tools-NSzbC6Nr.js";
1
+ import { l as toolResultToText } from "./types-BIarq1qE.js";
2
+ import { Ct as listFiles, Dt as edit, Et as glob, Ht as readKeybindingsRaw, I as EMPTY_EXTENSION_REGISTRY, St as multiEdit, Tt as grep, Ut as resolveExtensionKeybindings, Y as buildExtensionPreset, at as definePreset, ct as writeFile$1, ft as createSpawnTool, it as composePresets, kt as resolveStorageDirs, xt as readFile$1, yt as shell, zt as mergeKeybindings } from "./agent-DkSmGkDM.js";
4
3
  import { c as shortId, r as fmtTokens, s as previewLine } from "./format-BNOXpl-1.js";
5
4
  import { l as errorMessage } from "./errors-DJUxZg9b.js";
6
- import { joinSystemPrompt } from "./prompt.js";
5
+ import { M as findGitRoot$1, i as discoverSkills, j as projectUserPaths } from "./resolve-Cr8JSL_O.js";
7
6
  import { t as writeFileAtomic } from "./atomic-write-B7zr1ddF.js";
8
- import { o as discoverSkills } from "./interpolate-BtIgcCuz.js";
9
7
  import { r as formatTokenUsage } from "./stats-DAKBEKjc.js";
10
- import { a as normalizeMcpServers, n as connectMcpServers } from "./mcp-Dn5W65Lv.js";
11
- import { n as definePreset, t as composePresets } from "./presets-DtyUA1-S.js";
8
+ import { joinSystemPrompt } from "./prompt.js";
9
+ import { a as normalizeMcpServers, n as connectMcpServers } from "./mcp-CcvuVXB9.js";
10
+ import { i as findActiveTriggers, n as collectReferences, o as mergeSuggestions, t as applyInsert } from "./completion-core-CXYSVhZo.js";
12
11
  import { r as fetchUrl } from "./fetch-url-CWE8X5OD.js";
13
12
  import { webSearch } from "./tools/web-search.js";
14
- import { b as modelsForDescriptor, i as credentialsPath, n as resolveStorageDirs, r as applyApiKeyEnv, t as detectAuth, u as BUILTIN_PROVIDERS, w as restoreModelOptions, y as modelSupportsReasoning } from "./auth-D9uj0nNr.js";
15
- import { C as collectReferences, J as parseEditOutcomesFromResult, S as applyInsert, et as chatReferenceTokensFromText, u as ownerOf, w as findActiveTrigger, z as extractEditPayload } from "./turn-operations-tG0vuBNc.js";
13
+ import { C as restoreModelOptions, l as BUILTIN_PROVIDERS, n as applyApiKeyEnv, r as credentialsPath, t as detectAuth, v as modelSupportsReasoning, y as modelsForDescriptor } from "./auth-BDSu_0t3.js";
14
+ import { H as extractEditPayload, Z as parseEditOutcomesFromResult, p as ownerOf, rt as chatReferenceTokensFromText, u as capTranscriptEvents } from "./turn-operations-Dq9Kx7m2.js";
16
15
  import { createRequire } from "node:module";
17
16
  import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
18
17
  import { Buffer as Buffer$1 } from "node:buffer";
19
- import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
18
+ import { chmodSync, createReadStream, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
20
19
  import { readdir, stat, writeFile } from "node:fs/promises";
21
20
  import { spawn } from "node:child_process";
22
- import { createHash } from "node:crypto";
23
21
  import { homedir, tmpdir } from "node:os";
22
+ import { createHash } from "node:crypto";
24
23
  import { createGunzip } from "node:zlib";
25
24
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
26
25
  import { jsx } from "react/jsx-runtime";
@@ -201,7 +200,7 @@ function eventsFromTurns(turns, runs = []) {
201
200
  }
202
201
  }
203
202
  const pending = pendingToolCallsPerRun.get(rid) ?? /* @__PURE__ */ new Set();
204
- for (const block of turn.content) if (block.type === "tool_call") pending.add(block.id);
203
+ for (const block of turn.content) if (block.type === "tool_call" && /spawn/i.test(block.name)) pending.add(block.id);
205
204
  else if (block.type === "tool_result") pending.delete(block.callId);
206
205
  pendingToolCallsPerRun.set(rid, pending);
207
206
  }
@@ -311,7 +310,21 @@ function eventsFromTurns(turns, runs = []) {
311
310
  size: sibling.text.length
312
311
  });
313
312
  }
314
- events.push({
313
+ const commandOrigin = turn.runId ? runById.get(turn.runId)?.origin : void 0;
314
+ const commandDisplay = commandOrigin?.kind === "command" ? commandOrigin.display : void 0;
315
+ if (commandDisplay) {
316
+ events.push({
317
+ kind: "user-prompt",
318
+ text: commandDisplay,
319
+ turnId: turn.id
320
+ });
321
+ events.push({
322
+ kind: "command-detail",
323
+ label: "prompt",
324
+ text: block.text,
325
+ ...tag
326
+ });
327
+ } else events.push({
315
328
  kind: "user-prompt",
316
329
  text: block.text,
317
330
  turnId: turn.id,
@@ -333,6 +346,46 @@ function eventsFromTurns(turns, runs = []) {
333
346
  lastDepthAtEmission = depth;
334
347
  continue;
335
348
  }
349
+ if (turn.role === "command") {
350
+ if (lastDepthAtEmission === 0) events.push({
351
+ kind: "separator",
352
+ text: ""
353
+ });
354
+ for (const block of turn.content) if (block.type === "text" && block.text.trim()) events.push({
355
+ kind: "markdown",
356
+ text: block.text,
357
+ streaming: false,
358
+ ...tag
359
+ });
360
+ else if (block.type === "tool_result") {
361
+ const meta = block.metadata?.command;
362
+ const commandMeta = meta && typeof meta === "object" && !Array.isArray(meta) ? meta : void 0;
363
+ const customMeta = commandMeta?.custom;
364
+ if (customMeta && typeof customMeta === "object" && !Array.isArray(customMeta) && typeof customMeta.renderer === "string") {
365
+ const bound = customMeta;
366
+ events.push({
367
+ kind: "custom",
368
+ text: toolResultText(block.output),
369
+ custom: {
370
+ renderer: bound.renderer,
371
+ ...bound.data !== void 0 ? { data: bound.data } : {}
372
+ },
373
+ ...tag
374
+ });
375
+ continue;
376
+ }
377
+ const label = typeof commandMeta?.label === "string" ? commandMeta.label : void 0;
378
+ events.push({
379
+ kind: "command-detail",
380
+ text: toolResultText(block.output),
381
+ ...label ? { label } : {},
382
+ ...commandMeta?.syntax === "diff" ? { syntax: "diff" } : {},
383
+ ...tag
384
+ });
385
+ }
386
+ lastDepthAtEmission = depth;
387
+ continue;
388
+ }
336
389
  if (turn.role === "assistant") {
337
390
  for (const block of turn.content) if (block.type === "text" && block.text.trim()) events.push({
338
391
  kind: "markdown",
@@ -559,12 +612,14 @@ const MARGIN_TOP = {
559
612
  "thinking": 1,
560
613
  "tool": 1,
561
614
  "tool-result": 0,
615
+ "command-detail": 1,
562
616
  "error": 1,
563
617
  "markdown": 1,
564
618
  "spawn-start": 1,
565
619
  "spawn-end": 0,
566
620
  "compact-summary": 1,
567
- "task-notification": 1
621
+ "task-notification": 1,
622
+ "custom": 1
568
623
  };
569
624
  const TOOL_KINDS = new Set(["tool", "tool-result"]);
570
625
  /**
@@ -1918,62 +1973,6 @@ function singleAgentRegistry(preset) {
1918
1973
  } };
1919
1974
  }
1920
1975
  //#endregion
1921
- //#region src/chat/project-root.ts
1922
- /**
1923
- * Git root detection — walks parents from `cwd` looking for a `.git`
1924
- * entry, returning the absolute path of the repo root or `null` when
1925
- * the search reaches the filesystem root.
1926
- *
1927
- * Why parent-walk over `git rev-parse --show-toplevel`:
1928
- *
1929
- * - Zero dependencies (no shell-out, no git binary requirement,
1930
- * works even when `git` is missing from `PATH`).
1931
- * - Pure-sync, deterministic, easy to test against tmp dirs.
1932
- * - Equally fast in practice — most lookups are a single `existsSync`.
1933
- *
1934
- * Recognizes `.git` as either:
1935
- *
1936
- * - A directory (standard repo layout).
1937
- * - A file containing `gitdir: …` (worktrees + submodules).
1938
- *
1939
- * Bare repos (no working tree) are intentionally NOT detected here —
1940
- * the TUI's data dir contract is "project working tree", and a bare
1941
- * repo has no working files to scope sessions against.
1942
- */
1943
- /**
1944
- * Walk parents of `cwd` looking for a `.git` entry. Returns the
1945
- * absolute path of the directory containing `.git`, or `null` when no
1946
- * git repo is found above `cwd`.
1947
- *
1948
- * Stops at the filesystem root (`/`) — no infinite loop on `dirname`'s
1949
- * idempotent root case (`dirname('/')` returns `/`).
1950
- */
1951
- function findGitRoot$1(cwd = process.cwd()) {
1952
- let dir = resolve(cwd);
1953
- for (let depth = 0; depth < 64; depth++) {
1954
- if (hasGitMarker(dir)) return dir;
1955
- const parent = dirname(dir);
1956
- if (parent === dir) return null;
1957
- dir = parent;
1958
- }
1959
- return null;
1960
- }
1961
- /**
1962
- * `.git` exists as a directory (standard repo) OR as a file containing
1963
- * a `gitdir:` pointer (worktree / submodule). Tolerant on stat failures
1964
- * — a permission error on a parent directory shouldn't crash the walk.
1965
- */
1966
- function hasGitMarker(dir) {
1967
- const candidate = resolve(dir, ".git");
1968
- try {
1969
- if (!existsSync(candidate)) return false;
1970
- const stat = statSync(candidate);
1971
- return stat.isDirectory() || stat.isFile();
1972
- } catch {
1973
- return false;
1974
- }
1975
- }
1976
- //#endregion
1977
1976
  //#region src/chat/agents-md.ts
1978
1977
  /**
1979
1978
  * Personal instructions discovery — load user + project `AGENTS.md` /
@@ -2155,7 +2154,11 @@ function chatAutoCompactBehavior(opts) {
2155
2154
  const contextWindow = typeof opts.rawContextWindow === "number" && Number.isFinite(opts.rawContextWindow) && opts.rawContextWindow > 0 ? opts.rawContextWindow : null;
2156
2155
  return {
2157
2156
  threshold: opts.threshold,
2158
- ...contextWindow ? { contextWindow } : {}
2157
+ ...contextWindow ? { contextWindow } : {},
2158
+ ...opts.summarizer ? {
2159
+ provider: opts.summarizer.provider,
2160
+ model: opts.summarizer.model
2161
+ } : {}
2159
2162
  };
2160
2163
  }
2161
2164
  //#endregion
@@ -3061,17 +3064,22 @@ function useCompletion(input, providers, options = {}) {
3061
3064
  const { text, cursor } = input;
3062
3065
  const { maxQueryLength } = options;
3063
3066
  const [dismissedTriggerStart, setDismissedTriggerStart] = useState(null);
3064
- const rawTrigger = useMemo(() => findActiveTrigger(text, cursor, providers, { maxQueryLength }), [
3067
+ const rawTrigger = useMemo(() => findActiveTriggers(text, cursor, providers, { maxQueryLength }), [
3065
3068
  text,
3066
3069
  cursor,
3067
3070
  providers,
3068
3071
  maxQueryLength
3069
3072
  ]);
3070
- const active = useMemo(() => {
3073
+ const activeMulti = useMemo(() => {
3071
3074
  if (!rawTrigger) return null;
3072
3075
  if (rawTrigger.span.start === dismissedTriggerStart) return null;
3073
3076
  return rawTrigger;
3074
3077
  }, [rawTrigger, dismissedTriggerStart]);
3078
+ const active = useMemo(() => activeMulti ? {
3079
+ provider: activeMulti.providers[0],
3080
+ query: activeMulti.query,
3081
+ span: activeMulti.span
3082
+ } : null, [activeMulti]);
3075
3083
  useEffect(() => {
3076
3084
  if (!rawTrigger) setDismissedTriggerStart(null);
3077
3085
  }, [rawTrigger]);
@@ -3081,7 +3089,7 @@ function useCompletion(input, providers, options = {}) {
3081
3089
  const abortRef = useRef(null);
3082
3090
  useEffect(() => {
3083
3091
  abortRef.current?.abort();
3084
- if (!active) {
3092
+ if (!activeMulti) {
3085
3093
  setItems([]);
3086
3094
  setLoading(false);
3087
3095
  setSelectedIndex(0);
@@ -3094,30 +3102,35 @@ function useCompletion(input, providers, options = {}) {
3094
3102
  cursor
3095
3103
  };
3096
3104
  let cancelled = false;
3097
- const out = active.provider.suggest(active.query, ctx, controller.signal);
3098
- if (Array.isArray(out)) {
3099
- setItems(out);
3105
+ const outs = activeMulti.providers.map((provider) => ({
3106
+ provider,
3107
+ out: provider.suggest(activeMulti.query, ctx, controller.signal)
3108
+ }));
3109
+ if (outs.every((o) => Array.isArray(o.out))) {
3110
+ setItems(mergeSuggestions(outs.map((o) => ({
3111
+ provider: o.provider,
3112
+ items: o.out
3113
+ }))));
3100
3114
  setSelectedIndex(0);
3101
3115
  setLoading(false);
3102
3116
  return;
3103
3117
  }
3104
3118
  setLoading(true);
3105
- out.then((next) => {
3119
+ Promise.allSettled(outs.map(async (o) => ({
3120
+ provider: o.provider,
3121
+ items: await o.out
3122
+ }))).then((results) => {
3106
3123
  if (cancelled) return;
3107
- setItems(next);
3124
+ setItems(mergeSuggestions(results.flatMap((r) => r.status === "fulfilled" ? [r.value] : [])));
3108
3125
  setSelectedIndex(0);
3109
3126
  setLoading(false);
3110
- }, () => {
3111
- if (cancelled) return;
3112
- setItems([]);
3113
- setLoading(false);
3114
3127
  });
3115
3128
  return () => {
3116
3129
  cancelled = true;
3117
3130
  controller.abort();
3118
3131
  };
3119
3132
  }, [
3120
- active,
3133
+ activeMulti,
3121
3134
  text,
3122
3135
  cursor
3123
3136
  ]);
@@ -3167,538 +3180,6 @@ function useCompletion(input, providers, options = {}) {
3167
3180
  ]);
3168
3181
  }
3169
3182
  //#endregion
3170
- //#region src/chat/keybindings.ts
3171
- /**
3172
- * Keybindings — a single source of truth for the app's global shortcuts.
3173
- *
3174
- * Every customizable action is declared once in {@link KEYBINDING_DEFS}
3175
- * with its default trigger, label, and description. The TUI imports
3176
- * `DEFAULT_KEYBINDINGS` for the baked-in defaults; on launch we load
3177
- * `<userDir>/keybindings.json` (when present) and merge user overrides
3178
- * on top so the user can rebind any action without recompiling.
3179
- *
3180
- * Scope (deliberate):
3181
- *
3182
- * - We expose only the **global, action-opening** shortcuts here —
3183
- * "open the Settings modal", "cycle agent profile", "enter
3184
- * select-turn mode", etc. Modal-internal letter shortcuts (`f`
3185
- * fork, `d` delete, `c` copy on the turn-details modal) and
3186
- * universal navigation (`↑/↓/↵/⎋`) stay hardcoded — they live
3187
- * too close to a renderable's input contract to remap safely.
3188
- *
3189
- * - Storage lives at the **user level** (`<userDir>/keybindings.json`),
3190
- * never inside a project's `.{prefix}/`. Shortcut muscle memory is
3191
- * a per-user concern, not a per-repo one.
3192
- *
3193
- * Format on disk: JSON with `//` and `/* … *\/` comments allowed
3194
- * (parsed via a small JSONC stripper). Each entry maps an action id
3195
- * to a binding spec string. Specs are parsed by {@link parseBindingSpec};
3196
- * unknown / malformed entries fall back to the default for that action
3197
- * (with a debug log under `ZIDANE_DEBUG`) so a syntax slip never bricks
3198
- * the TUI.
3199
- */
3200
- /**
3201
- * The canonical action catalog. Order is the source of truth for the
3202
- * generated keybindings file and the settings UI.
3203
- */
3204
- const KEYBINDING_DEFS = [
3205
- {
3206
- action: "openSettings",
3207
- default: "ctrl+o",
3208
- label: "settings",
3209
- description: "open the Settings modal (toggles, theme, keybindings)",
3210
- group: "Global"
3211
- },
3212
- {
3213
- action: "openSessionDetails",
3214
- default: "ctrl+shift+x",
3215
- label: "session",
3216
- description: "open the session details modal (stats, export, delete, rename)",
3217
- group: "Global"
3218
- },
3219
- {
3220
- action: "newSession",
3221
- default: "ctrl+n",
3222
- label: "new session",
3223
- description: "start a fresh chat session with the active provider/model (same as the \"+ new session\" row)",
3224
- group: "Global"
3225
- },
3226
- {
3227
- action: "openModelPicker",
3228
- default: "ctrl+m",
3229
- label: "model",
3230
- description: "open the cross-provider model picker",
3231
- group: "Global"
3232
- },
3233
- {
3234
- action: "openEffortPicker",
3235
- default: "ctrl+l",
3236
- label: "options",
3237
- description: "open the model-options picker — reasoning effort + feature toggles (e.g. fast mode)",
3238
- group: "Global"
3239
- },
3240
- {
3241
- action: "openTodos",
3242
- default: "ctrl+t",
3243
- label: "todos",
3244
- description: "open the active run's todo list (the agent's `todowrite` checkpoints)",
3245
- group: "Global"
3246
- },
3247
- {
3248
- action: "openContextPanel",
3249
- default: "ctrl+shift+g",
3250
- label: "context",
3251
- description: "open the context-usage breakdown (what is taking up the context window)",
3252
- group: "Global"
3253
- },
3254
- {
3255
- action: "openKeybindings",
3256
- default: "ctrl+y",
3257
- label: "keybindings",
3258
- description: "open the keybindings panel — list every shortcut and jump to the keybindings.json file",
3259
- group: "Global"
3260
- },
3261
- {
3262
- action: "cycleAgent",
3263
- default: "shift+tab",
3264
- label: "cycle agent",
3265
- description: "cycle to the next agent profile (when multiple profiles are registered)",
3266
- group: "Global"
3267
- },
3268
- {
3269
- action: "enterSelectTurnMode",
3270
- default: "ctrl+s",
3271
- label: "messages",
3272
- description: "enter select-turn mode to navigate previous messages",
3273
- group: "Global"
3274
- },
3275
- {
3276
- action: "cancelToolCall",
3277
- default: "ctrl+k",
3278
- label: "cancel tool",
3279
- description: "open the in-flight tool picker to cancel a single tool call without aborting the run (esc still aborts the whole run)",
3280
- group: "Global"
3281
- },
3282
- {
3283
- action: "changeCwd",
3284
- default: "ctrl+h",
3285
- label: "cwd",
3286
- description: "open the directory picker to change the working directory",
3287
- group: "Global"
3288
- },
3289
- {
3290
- action: "attachClipboardImage",
3291
- default: "ctrl+v",
3292
- label: "paste image",
3293
- description: "attach an image from the OS clipboard to the prompt (needs a platform helper: pngpaste on macOS, wl-clipboard/xclip on Linux)",
3294
- group: "Global"
3295
- },
3296
- {
3297
- action: "openExternalEditor",
3298
- default: "ctrl+g",
3299
- label: "edit in $EDITOR",
3300
- description: "open the current prompt in $EDITOR / $VISUAL, reloading it when you save & quit (git-style); requires the \"Edit prompt in $EDITOR\" setting",
3301
- group: "Global"
3302
- },
3303
- {
3304
- action: "enterQueueSelection",
3305
- default: "",
3306
- label: "select queue",
3307
- description: "move focus from the prompt textarea into the type-ahead queue box (also: ↑ on an empty prompt)",
3308
- group: "Message queue"
3309
- },
3310
- {
3311
- action: "pushQueuedMessage",
3312
- default: "ctrl+return",
3313
- label: "push",
3314
- description: "steer the selected queued message into the live run (delivered between tool calls)",
3315
- group: "Message queue"
3316
- },
3317
- {
3318
- action: "dropQueuedMessage",
3319
- default: "backspace",
3320
- label: "drop",
3321
- description: "remove the selected queued message — exits back to the prompt when the queue empties (the big \"remove\" key works everywhere: backspace on Win/Linux, the laptop key labeled \"delete\" on Mac, which sends backspace)",
3322
- group: "Message queue"
3323
- },
3324
- {
3325
- action: "turnFork",
3326
- default: "f",
3327
- label: "fork",
3328
- description: "fork the session at the selected turn (two-press confirm)",
3329
- group: "Turn details"
3330
- },
3331
- {
3332
- action: "turnDelete",
3333
- default: "d",
3334
- label: "delete",
3335
- description: "delete the selected turn (two-press confirm)",
3336
- group: "Turn details"
3337
- },
3338
- {
3339
- action: "turnCopy",
3340
- default: "c",
3341
- label: "copy",
3342
- description: "copy the selected turn content to the clipboard (OSC 52)",
3343
- group: "Turn details"
3344
- },
3345
- {
3346
- action: "turnEdit",
3347
- default: "e",
3348
- label: "edit",
3349
- description: "edit the text content of the selected turn",
3350
- group: "Turn details"
3351
- },
3352
- {
3353
- action: "sessionDelete",
3354
- default: "d",
3355
- label: "delete",
3356
- description: "delete the entire session (two-press confirm)",
3357
- group: "Session details"
3358
- },
3359
- {
3360
- action: "sessionCopyId",
3361
- default: "c",
3362
- label: "copy id",
3363
- description: "copy the full session id to the clipboard (OSC 52)",
3364
- group: "Session details"
3365
- },
3366
- {
3367
- action: "sessionGenerateTitle",
3368
- default: "g",
3369
- label: "generate title",
3370
- description: "generate a title for the session via the active provider/model",
3371
- group: "Session details"
3372
- },
3373
- {
3374
- action: "sessionExportMarkdown",
3375
- default: "e",
3376
- label: "export md",
3377
- description: "export the session as Markdown under .{prefix}/sessions/",
3378
- group: "Session details"
3379
- },
3380
- {
3381
- action: "sessionExportJson",
3382
- default: "j",
3383
- label: "export json",
3384
- description: "export the session as JSON under .{prefix}/sessions/",
3385
- group: "Session details"
3386
- },
3387
- {
3388
- action: "sessionCompact",
3389
- default: "k",
3390
- label: "compact",
3391
- description: "compact older turns via an LLM summary (model still sees everything from the boundary down)",
3392
- group: "Session details"
3393
- }
3394
- ];
3395
- /** Index by action for O(1) lookup of label / description / default. */
3396
- const KEYBINDING_DEF_BY_ACTION = KEYBINDING_DEFS.reduce((acc, def) => Object.assign(acc, { [def.action]: def }), {});
3397
- /** Defaults, derived from {@link KEYBINDING_DEFS} so the two never drift. */
3398
- const DEFAULT_KEYBINDINGS = KEYBINDING_DEFS.reduce((acc, def) => Object.assign(acc, { [def.action]: def.default }), {});
3399
- /**
3400
- * Aliases the parser accepts on input so the on-disk format can be
3401
- * forgiving (different terminals + docs label modifiers + keys
3402
- * differently). Output names are canonicalized to one form per key so
3403
- * `matchesBinding` only has to compare one canonical against a
3404
- * KeyEvent's `name`. Unknown names pass through verbatim — that's
3405
- * useful for terminal-specific keys we don't have to enumerate.
3406
- */
3407
- const NAME_ALIASES = {
3408
- esc: "escape",
3409
- enter: "return",
3410
- ret: "return",
3411
- space: "space",
3412
- spc: "space",
3413
- del: "delete",
3414
- ins: "insert",
3415
- pgup: "pageup",
3416
- pgdn: "pagedown",
3417
- pagup: "pageup",
3418
- pagedn: "pagedown"
3419
- };
3420
- const MODIFIER_ALIASES = {
3421
- ctrl: "ctrl",
3422
- control: "ctrl",
3423
- shift: "shift",
3424
- alt: "alt",
3425
- option: "alt",
3426
- opt: "alt",
3427
- meta: "meta",
3428
- cmd: "meta",
3429
- command: "meta",
3430
- super: "meta",
3431
- win: "meta"
3432
- };
3433
- /**
3434
- * Parse a `'ctrl+shift+o'`-style spec into structured fields. Returns
3435
- * `null` for empty / malformed inputs; the caller falls back to the
3436
- * default for that action.
3437
- *
3438
- * Tolerant by design:
3439
- * - Case-insensitive (`Ctrl+O` → same as `ctrl+o`).
3440
- * - Modifier order doesn't matter (`shift+ctrl+o` works).
3441
- * - Mixed `+` and `-` separators allowed (`ctrl-o`, `ctrl+o`).
3442
- * - Trailing / leading whitespace is stripped.
3443
- * - Duplicate modifiers collapse silently.
3444
- */
3445
- function parseBindingSpec(spec) {
3446
- if (typeof spec !== "string") return null;
3447
- const trimmed = spec.trim().toLowerCase();
3448
- if (!trimmed) return null;
3449
- const parts = trimmed.split(/[+\-]/).map((p) => p.trim()).filter(Boolean);
3450
- if (parts.length === 0) return null;
3451
- const result = {
3452
- ctrl: false,
3453
- shift: false,
3454
- alt: false,
3455
- meta: false,
3456
- name: ""
3457
- };
3458
- for (const part of parts) {
3459
- const modifier = MODIFIER_ALIASES[part];
3460
- if (modifier) {
3461
- result[modifier] = true;
3462
- continue;
3463
- }
3464
- if (result.name) return null;
3465
- result.name = NAME_ALIASES[part] ?? part;
3466
- }
3467
- if (!result.name) return null;
3468
- return result;
3469
- }
3470
- /**
3471
- * Test a KeyEvent against a parsed-or-string binding. Accepts either
3472
- * form so callers can pre-parse (hot path) or pass the raw spec for
3473
- * one-off checks.
3474
- *
3475
- * Modifier semantics: ALL declared modifiers must be pressed AND no
3476
- * extra modifiers may be pressed. So `'o'` does NOT match `ctrl+o`,
3477
- * and `'ctrl+o'` does NOT match `ctrl+shift+o`. This avoids surprises
3478
- * where a binding that should be specific accidentally fires for a
3479
- * superset of key combinations.
3480
- */
3481
- function matchesBinding(event, spec) {
3482
- const parsed = typeof spec === "string" ? parseBindingSpec(spec) : spec;
3483
- if (!parsed) return false;
3484
- const altPressed = !!event.option || !!event.alt;
3485
- if (parsed.ctrl !== !!event.ctrl) return false;
3486
- if (parsed.shift !== !!event.shift) return false;
3487
- if (parsed.alt !== altPressed) return false;
3488
- if (parsed.meta !== !!event.meta) return false;
3489
- if ((event.name ?? "").toLowerCase() !== parsed.name) return false;
3490
- return true;
3491
- }
3492
- /** Verbose key names → compact single-cell glyphs used by hint rows. */
3493
- const KEY_GLYPHS = {
3494
- up: "↑",
3495
- down: "↓",
3496
- left: "←",
3497
- right: "→",
3498
- return: "↵",
3499
- enter: "↵",
3500
- delete: "⌫",
3501
- backspace: "⌫",
3502
- escape: "esc",
3503
- space: "␣",
3504
- tab: "⇥"
3505
- };
3506
- /**
3507
- * Render a binding spec (`"ctrl+return"`, `"backspace"`, `"delete"`) as a
3508
- * compact display string the user recognizes at a glance. Substitutes
3509
- * arrow / enter / backspace glyphs for their verbose names so the hint
3510
- * row stays narrow; modifier names + plain keys pass through unchanged.
3511
- *
3512
- * Empty / missing specs return an empty string — callers decide whether
3513
- * to render a placeholder (e.g. `'—'` for an unbound action in the
3514
- * keybindings panel) or hide the surface entirely (e.g. hint rows that
3515
- * fall back to a different chord when the binding is empty).
3516
- */
3517
- function formatBindingForDisplay(spec) {
3518
- if (!spec) return "";
3519
- const segments = spec.toLowerCase().split("+");
3520
- const key = segments.pop() ?? "";
3521
- const modifiers = segments.join("+");
3522
- const glyph = KEY_GLYPHS[key] ?? key;
3523
- return modifiers ? `${modifiers}+${glyph}` : glyph;
3524
- }
3525
- /**
3526
- * Merge a partial map of user overrides into the defaults. Unknown
3527
- * action keys are dropped (a future TUI version may have retired the
3528
- * action); invalid spec strings (parse fails) fall back to the default
3529
- * for that action.
3530
- */
3531
- function mergeKeybindings(overrides) {
3532
- if (!overrides || typeof overrides !== "object") return { ...DEFAULT_KEYBINDINGS };
3533
- const merged = { ...DEFAULT_KEYBINDINGS };
3534
- for (const def of KEYBINDING_DEFS) {
3535
- const raw = overrides[def.action];
3536
- if (typeof raw !== "string") continue;
3537
- if (!parseBindingSpec(raw)) continue;
3538
- merged[def.action] = raw;
3539
- }
3540
- return merged;
3541
- }
3542
- /** Path of the user-level keybindings file. */
3543
- function keybindingsPath(userDir) {
3544
- return resolve(userDir, "keybindings.json");
3545
- }
3546
- /**
3547
- * Walk {@link KEYBINDING_DEFS} in order and bucket rows into contiguous
3548
- * sections by `group`. Shared between the standalone `KeybindingsModal`
3549
- * and the `Keybindings` tab inside `SettingsModal` so both surfaces
3550
- * render the same catalog with the same ordering rules.
3551
- */
3552
- function groupBindings(bindings) {
3553
- const sections = [];
3554
- for (const def of KEYBINDING_DEFS) {
3555
- const last = sections[sections.length - 1];
3556
- const spec = bindings[def.action] ?? "";
3557
- if (last && last.group === def.group) {
3558
- last.rows.push({
3559
- def,
3560
- spec
3561
- });
3562
- continue;
3563
- }
3564
- sections.push({
3565
- group: def.group,
3566
- rows: [{
3567
- def,
3568
- spec
3569
- }]
3570
- });
3571
- }
3572
- return sections;
3573
- }
3574
- /**
3575
- * Width (in cells) of the fixed key column used by every binding-row
3576
- * renderer. Derived once from {@link KEYBINDING_DEFS} so a new action
3577
- * with a wider default spec (`ctrl+shift+x`, …) automatically grows
3578
- * the column instead of truncating the label.
3579
- */
3580
- const KEYBINDING_KEY_COL_WIDTH = (() => {
3581
- let max = 8;
3582
- for (const def of KEYBINDING_DEFS) {
3583
- const width = formatBindingForDisplay(def.default).length;
3584
- if (width > max) max = width;
3585
- }
3586
- return max + 2;
3587
- })();
3588
- /**
3589
- * Load + merge the user's keybindings file (when present). Always
3590
- * returns a fully-resolved {@link KeyBindings} — defaults fill in for
3591
- * any missing / invalid entries so consumers can treat the result as
3592
- * total.
3593
- *
3594
- * Failure modes (all tolerated, all fall back to defaults):
3595
- * - File missing
3596
- * - File not valid JSON / JSONC (after comment stripping)
3597
- * - Top-level is not an object
3598
- * - Individual entries have invalid spec strings
3599
- */
3600
- function readKeybindings(userDir) {
3601
- const path = keybindingsPath(userDir);
3602
- if (!existsSync(path)) return { ...DEFAULT_KEYBINDINGS };
3603
- try {
3604
- const raw = readFileSync(path, "utf-8");
3605
- const parsed = JSON.parse(stripJsonComments(raw));
3606
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { ...DEFAULT_KEYBINDINGS };
3607
- return mergeKeybindings(parsed);
3608
- } catch {
3609
- return { ...DEFAULT_KEYBINDINGS };
3610
- }
3611
- }
3612
- /**
3613
- * Create the user-level `keybindings.json` if it doesn't exist,
3614
- * prefilled with every default. Returns the absolute file path either
3615
- * way so the caller can open it in `$EDITOR`. Idempotent: an existing
3616
- * user file is NEVER overwritten — the user's customizations are
3617
- * sacred.
3618
- */
3619
- function ensureKeybindingsFile(userDir) {
3620
- const path = keybindingsPath(userDir);
3621
- if (existsSync(path)) return path;
3622
- writeFileAtomic(path, renderDefaultFile(), { ensureDir: true });
3623
- return path;
3624
- }
3625
- /**
3626
- * Render the default `keybindings.json` body. JSONC with a header
3627
- * explaining the format + per-action comments derived from
3628
- * {@link KEYBINDING_DEFS}'s descriptions. The user sees the catalog +
3629
- * defaults at a glance the moment they open the file.
3630
- */
3631
- function renderDefaultFile() {
3632
- const lines = [];
3633
- lines.push("// Zidane keybindings — edit values below to customize.");
3634
- lines.push("//");
3635
- lines.push("// Format:");
3636
- lines.push("// \"<action>\": \"<modifier>+<modifier>+<key>\"");
3637
- lines.push("//");
3638
- lines.push("// Modifiers: ctrl, shift, alt (option), meta (cmd).");
3639
- lines.push("// Keys: a-z, 0-9, tab, return, escape, space, delete, …");
3640
- lines.push("//");
3641
- lines.push("// Comments (`//` line, `/* block */`) are stripped before parsing.");
3642
- lines.push("// Restart the TUI to apply changes.");
3643
- lines.push("{");
3644
- KEYBINDING_DEFS.forEach((def, i) => {
3645
- const trailingComma = i < KEYBINDING_DEFS.length - 1 ? "," : "";
3646
- lines.push(` // ${def.description}`);
3647
- lines.push(` ${JSON.stringify(def.action)}: ${JSON.stringify(def.default)}${trailingComma}`);
3648
- });
3649
- lines.push("}");
3650
- lines.push("");
3651
- return lines.join("\n");
3652
- }
3653
- /**
3654
- * Strip `//` line comments and `/* … *\/` block comments from a JSONC
3655
- * source string. Stays out of string literals so a `"foo // bar"` value
3656
- * keeps its slashes. Exported for unit-tests; the only runtime caller
3657
- * is {@link readKeybindings}.
3658
- *
3659
- * Trade-off: this is a tiny state-machine, not a full JSONC parser. It
3660
- * doesn't understand `\\` outside strings (irrelevant for JSON) and
3661
- * doesn't track line numbers in error messages. Good enough for a
3662
- * config file — corruption in production gracefully falls back to the
3663
- * defaults via the try/catch in `readKeybindings`.
3664
- */
3665
- function stripJsonComments(input) {
3666
- let out = "";
3667
- let i = 0;
3668
- let inString = false;
3669
- let escape = false;
3670
- while (i < input.length) {
3671
- const c = input[i];
3672
- if (inString) {
3673
- out += c;
3674
- if (escape) escape = false;
3675
- else if (c === "\\") escape = true;
3676
- else if (c === "\"") inString = false;
3677
- i++;
3678
- continue;
3679
- }
3680
- if (c === "\"") {
3681
- inString = true;
3682
- out += c;
3683
- i++;
3684
- continue;
3685
- }
3686
- if (c === "/" && input[i + 1] === "/") {
3687
- while (i < input.length && input[i] !== "\n") i++;
3688
- continue;
3689
- }
3690
- if (c === "/" && input[i + 1] === "*") {
3691
- i += 2;
3692
- while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
3693
- i += 2;
3694
- continue;
3695
- }
3696
- out += c;
3697
- i++;
3698
- }
3699
- return out;
3700
- }
3701
- //#endregion
3702
3183
  //#region src/chat/user-config.ts
3703
3184
  /**
3704
3185
  * User-level config at `~/.{prefix}/config.json`. Holds global flags
@@ -3773,9 +3254,18 @@ function resolveConfig(options) {
3773
3254
  const store = typeof options.store === "function" ? options.store(paths) : options.store;
3774
3255
  const stateStore = createStateStore(paths.state);
3775
3256
  const initialState = stateStore.load();
3776
- const providers = options.providers ?? BUILTIN_PROVIDERS;
3257
+ const extensions = options.extensionRegistry ?? EMPTY_EXTENSION_REGISTRY;
3258
+ const baseProviders = options.providers ?? BUILTIN_PROVIDERS;
3259
+ const providers = wrapProvidersWithExtensionMiddleware(Object.keys(extensions.providers).length === 0 ? baseProviders : {
3260
+ ...extensions.providers,
3261
+ ...baseProviders
3262
+ }, extensions);
3777
3263
  if (options.agents && options.preset) throw new Error("resolveConfig: pass either `preset` (single-agent legacy) or `agents` (multi-profile registry), not both. Migrate `preset` into an entry inside `agents` if you want both worlds.");
3778
- const agents = options.agents ?? (options.preset ? singleAgentRegistry(options.preset) : BUILTIN_AGENTS);
3264
+ const baseAgents = options.agents ?? (options.preset ? singleAgentRegistry(options.preset) : BUILTIN_AGENTS);
3265
+ const agents = Object.keys(extensions.agents).length === 0 ? baseAgents : {
3266
+ ...extensions.agents,
3267
+ ...baseAgents
3268
+ };
3779
3269
  if (Object.keys(agents).length === 0) throw new Error("resolveConfig: `agents` registry is empty — at least one profile is required.");
3780
3270
  process.env.ZIDANE_CREDENTIALS_PATH = credentialsPath(paths.userDir);
3781
3271
  applyApiKeyEnv(paths.userDir, providers);
@@ -3788,7 +3278,9 @@ function resolveConfig(options) {
3788
3278
  }) : resumeProvider ? pickInitial(resumeProvider, providers, initialState) : null;
3789
3279
  const initialAgentId = resolveAgentId(agents, initialState.lastAgent, options.defaultAgent ?? "build");
3790
3280
  if (!initialAgentId) throw new Error("resolveConfig: failed to resolve an initial agent id from a non-empty registry.");
3791
- const keybindings = readKeybindings(paths.userDir);
3281
+ const rawKeybindings = readKeybindingsRaw(paths.userDir);
3282
+ const keybindings = mergeKeybindings(rawKeybindings);
3283
+ const extensionKeybindings = extensions.tuiContributions.keybindings.length === 0 ? [] : resolveExtensionKeybindings(extensions.tuiContributions.keybindings, rawKeybindings);
3792
3284
  return {
3793
3285
  prefix,
3794
3286
  storageDir,
@@ -3804,7 +3296,18 @@ function resolveConfig(options) {
3804
3296
  resumeProvider,
3805
3297
  initialPicked,
3806
3298
  keybindings,
3299
+ extensionKeybindings,
3807
3300
  autoUpdate: options.autoUpdate ?? null,
3301
+ extensions,
3302
+ extensionScanPaths: options.extensionScanPaths ?? [],
3303
+ extensionPreset: buildExtensionPreset(extensions),
3304
+ extensionSystemFragments: extensions.systemFragments,
3305
+ extensionSubagents: extensions.subagents,
3306
+ extensionSubagentOverrides: extensions.subagentOverrides,
3307
+ extensionConfigCategories: extensions.configCategories,
3308
+ extensionConfigItems: extensions.configItems,
3309
+ trustProjectExtensions: options.trustProjectExtensions ?? process.env.ZIDANE_TRUST_PROJECT_EXTENSIONS === "1",
3310
+ rawKeybindings,
3808
3311
  launch: options.launch ?? null
3809
3312
  };
3810
3313
  }
@@ -3924,6 +3427,33 @@ function makeModelsResolver(registry) {
3924
3427
  return descriptor ? modelsForDescriptor(descriptor) : [];
3925
3428
  };
3926
3429
  }
3430
+ function wrapProvidersWithExtensionMiddleware(providers, extensions) {
3431
+ if (extensions.providerMiddlewares.length === 0) return providers;
3432
+ const out = {};
3433
+ for (const [key, descriptor] of Object.entries(providers)) out[key] = {
3434
+ ...descriptor,
3435
+ factory: () => {
3436
+ let provider = descriptor.factory();
3437
+ for (const entry of extensions.providerMiddlewares) try {
3438
+ const next = entry.value(provider, {
3439
+ extensionName: entry.extensionName,
3440
+ source: entry.source,
3441
+ descriptor
3442
+ });
3443
+ if (isProviderLike(next)) provider = next;
3444
+ } catch (err) {
3445
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/extensions] ${entry.extensionName} provider middleware failed: ${errorMessage(err)}\n`);
3446
+ }
3447
+ return provider;
3448
+ }
3449
+ };
3450
+ return out;
3451
+ }
3452
+ function isProviderLike(value) {
3453
+ if (!value || typeof value !== "object") return false;
3454
+ const provider = value;
3455
+ return typeof provider.name === "string" && !!provider.meta && typeof provider.meta === "object" && typeof provider.formatTools === "function" && typeof provider.userMessage === "function" && typeof provider.assistantMessage === "function" && typeof provider.toolResultsMessage === "function" && typeof provider.stream === "function";
3456
+ }
3927
3457
  function resolveResumeProvider(state, providers, storageDir) {
3928
3458
  if (!state.lastProvider) {
3929
3459
  if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] resume: no \`lastProvider\` in state — auth screen will show. (Has \`${storageDir}/state.json\` been deleted or never written?)\n`);
@@ -5269,8 +4799,11 @@ const BUILTIN_THEMES = {
5269
4799
  [VAPORWAVE_THEME.id]: VAPORWAVE_THEME
5270
4800
  };
5271
4801
  /** Resolve a theme id to its full `Theme`, falling back to default on unknown ids. */
5272
- function resolveTheme(id) {
5273
- if (id && BUILTIN_THEMES[id]) return BUILTIN_THEMES[id];
4802
+ function resolveTheme(id, extraThemes) {
4803
+ if (id) {
4804
+ if (BUILTIN_THEMES[id]) return BUILTIN_THEMES[id];
4805
+ if (extraThemes && extraThemes[id]) return extraThemes[id];
4806
+ }
5274
4807
  return DEFAULT_THEME;
5275
4808
  }
5276
4809
  //#endregion
@@ -5280,6 +4813,7 @@ const DEFAULT_SETTINGS = {
5280
4813
  toolCallDisplay: "formatted",
5281
4814
  showToolResults: true,
5282
4815
  safeMode: true,
4816
+ loadProjectExtensions: false,
5283
4817
  hideSubagentOutput: true,
5284
4818
  theme: DEFAULT_THEME.id,
5285
4819
  showAllProjects: false,
@@ -5295,9 +4829,12 @@ const DEFAULT_SETTINGS = {
5295
4829
  maxWallMinutes: 0,
5296
4830
  showEditDiffs: true,
5297
4831
  editDiffDisplay: "full",
4832
+ showCompactionSummary: false,
4833
+ compactionModel: "",
5298
4834
  targetFps: 60,
5299
4835
  allowInteraction: true,
5300
4836
  smoothStreaming: true,
4837
+ fileLogging: false,
5301
4838
  showTodoIndicator: true,
5302
4839
  showThrobber: false,
5303
4840
  checkForUpdates: true,
@@ -5406,6 +4943,12 @@ const SETTINGS_TOGGLES = [
5406
4943
  label: "Persist large tool results",
5407
4944
  description: "write >8 KiB tool outputs to disk and inline a preview"
5408
4945
  },
4946
+ {
4947
+ category: "agent",
4948
+ key: "loadProjectExtensions",
4949
+ label: "Load project extensions",
4950
+ description: "run extensions under <project>/.zidane/extensions — off by default (they execute arbitrary local code)"
4951
+ },
5409
4952
  {
5410
4953
  category: "agent",
5411
4954
  key: "attachmentsEnabled",
@@ -5466,6 +5009,12 @@ const SETTINGS_TOGGLES = [
5466
5009
  label: "Edit diffs",
5467
5010
  description: "render edit / multi_edit / write_file as a unified diff"
5468
5011
  },
5012
+ {
5013
+ category: "ui",
5014
+ key: "showCompactionSummary",
5015
+ label: "Compaction summaries",
5016
+ description: "show the generated summary text under the ⎯⎯ compacted boundary row (off = metadata only)"
5017
+ },
5469
5018
  {
5470
5019
  category: "ui",
5471
5020
  key: "smoothStreaming",
@@ -5483,6 +5032,12 @@ const SETTINGS_TOGGLES = [
5483
5032
  key: "showThrobber",
5484
5033
  label: "Streaming throbber",
5485
5034
  description: "animated gradient glyphs at the transcript tail while the assistant is working"
5035
+ },
5036
+ {
5037
+ category: "ui",
5038
+ key: "fileLogging",
5039
+ label: "File logging",
5040
+ description: "persist this session's logs to ~/.zidane/logs/<datetime>.txt (the Logs tab shows them regardless)"
5486
5041
  }
5487
5042
  ];
5488
5043
  const SETTINGS_CHOICES = [
@@ -5834,7 +5389,7 @@ const SETTINGS_INPUTS = [{
5834
5389
  */
5835
5390
  function useEnabledToggleSet(opts) {
5836
5391
  const { settings, setSetting } = useSettings();
5837
- const { catalog, keyOf, settingKey } = opts;
5392
+ const { catalog, keyOf, settingKey, gcKeys } = opts;
5838
5393
  const persisted = settings[settingKey];
5839
5394
  const enabledSet = useMemo(() => {
5840
5395
  if (persisted === void 0) return new Set(catalog.map(keyOf));
@@ -5844,7 +5399,11 @@ function useEnabledToggleSet(opts) {
5844
5399
  catalog,
5845
5400
  keyOf
5846
5401
  ]);
5847
- const catalogKeys = useMemo(() => new Set(catalog.map(keyOf)), [catalog, keyOf]);
5402
+ const catalogKeys = useMemo(() => new Set(gcKeys ?? catalog.map(keyOf)), [
5403
+ gcKeys,
5404
+ catalog,
5405
+ keyOf
5406
+ ]);
5848
5407
  return {
5849
5408
  enabledSet,
5850
5409
  toggle: useCallback((name) => {
@@ -6174,10 +5733,6 @@ function buildHints(options) {
6174
5733
  label: inFlightToolCount === 1 ? "cancel task" : `cancel task (${inFlightToolCount})`
6175
5734
  } : null;
6176
5735
  if (uiMode === "minimal") return [
6177
- {
6178
- key: keybindings.openContextPanel,
6179
- label: "context"
6180
- },
6181
5736
  ...hasMultipleAgents ? [{
6182
5737
  key: keybindings.cycleAgent,
6183
5738
  label: agentLabel,
@@ -6190,10 +5745,6 @@ function buildHints(options) {
6190
5745
  }
6191
5746
  ];
6192
5747
  return [
6193
- {
6194
- key: keybindings.openContextPanel,
6195
- label: "context"
6196
- },
6197
5748
  ...hasMultipleAgents ? [{
6198
5749
  key: keybindings.cycleAgent,
6199
5750
  label: agentLabel,
@@ -6229,129 +5780,6 @@ function shortChord(spec) {
6229
5780
  return spec.startsWith("ctrl+") ? `/${spec.slice(5)}` : spec;
6230
5781
  }
6231
5782
  //#endregion
6232
- //#region src/chat/generate-title.ts
6233
- /** Hard cap on the result length. Anything longer is truncated client-side. */
6234
- const TITLE_MAX_CHARS = 60;
6235
- /** Default turn count fed to the model. 10 covers most exchanges without bloat. */
6236
- const DEFAULT_MAX_TURNS = 10;
6237
- /** Tokens budgeted for the model's reply. 64 fits any sane title with headroom. */
6238
- const TITLE_RESPONSE_MAX_TOKENS = 64;
6239
- /** Per-turn body cap when assembling the prompt — keeps the request payload tight. */
6240
- const PROMPT_TURN_CHAR_MAX = 2e3;
6241
- /**
6242
- * Drive the provider's `stream()` once and return a concise title for
6243
- * the conversation represented by `turns`. Throws when:
6244
- * - `turns` contains no text-bearing content (nothing to summarize),
6245
- * - the provider stream completes with no output text,
6246
- * - `signal` is aborted (rethrown verbatim).
6247
- *
6248
- * Output is trimmed, single-line, with surrounding quotes / trailing
6249
- * punctuation stripped, and truncated to `TITLE_MAX_CHARS`.
6250
- */
6251
- async function generateSessionTitle({ provider, model, turns, maxTurns = DEFAULT_MAX_TURNS, signal }) {
6252
- const transcript = renderTurnsForPrompt(turns.slice(-Math.max(1, maxTurns)));
6253
- if (!transcript) throw new Error("No text content in the recent turns to summarize.");
6254
- const system = [
6255
- "You generate concise, descriptive titles for chat conversations.",
6256
- "Reply with ONLY the title — no quotes, no markdown, no trailing punctuation,",
6257
- "no preamble (do not say \"Title:\" or similar).",
6258
- `Aim for 3 to 7 words and stay under ${TITLE_MAX_CHARS} characters.`
6259
- ].join(" ");
6260
- const userPrompt = `Generate a title for this conversation:\n\n${transcript}`;
6261
- let text = "";
6262
- const result = await provider.stream({
6263
- model,
6264
- system,
6265
- tools: [],
6266
- messages: [provider.userMessage(userPrompt)],
6267
- maxTokens: TITLE_RESPONSE_MAX_TOKENS,
6268
- ...signal ? { signal } : {}
6269
- }, { onText: (delta) => {
6270
- text += delta;
6271
- } });
6272
- return cleanTitle(text.trim().length > 0 ? text : result.text);
6273
- }
6274
- /**
6275
- * Decide whether a session is ready for one-shot automatic titling.
6276
- *
6277
- * Returns `true` iff the conversation has had its first exchange — at
6278
- * least two turns, i.e. the opening user message plus the assistant's
6279
- * reply (this is exactly the "N turns" count the sessions list shows) —
6280
- * AND no explicit title has been set yet. An existing `metadata.title`
6281
- * (a previous auto-generation, or a manual title from the session-details
6282
- * "generate title" action) is never clobbered.
6283
- *
6284
- * Pure + total. The runtime layers its own "is one already in flight?"
6285
- * and "is a provider picked?" guards on top of this; the predicate only
6286
- * encodes the content-level rule so it can be pinned in tests without a
6287
- * mock provider — same rationale as the exported {@link cleanTitle}.
6288
- */
6289
- function shouldAutoGenerateTitle(turns, metadata) {
6290
- if ((typeof metadata?.title === "string" ? metadata.title.trim() : "").length > 0) return false;
6291
- return turns.length >= 2;
6292
- }
6293
- /**
6294
- * Compact a turn list into a transcript-style prompt:
6295
- *
6296
- * user: …
6297
- * assistant: …
6298
- * user: …
6299
- *
6300
- * Tool calls and tool results are summarized to keep the request
6301
- * payload small — the model needs the rough flow of the conversation,
6302
- * not full JSON blobs. Per-turn text is clipped at `PROMPT_TURN_CHAR_MAX`
6303
- * so a single huge code dump doesn't blow the budget.
6304
- *
6305
- * Returns an empty string when the slice has no text-bearing content
6306
- * (e.g. a turn list that's only tool plumbing).
6307
- */
6308
- function renderTurnsForPrompt(turns) {
6309
- const lines = [];
6310
- for (const turn of turns) {
6311
- const body = textBodyOf(turn);
6312
- if (!body) continue;
6313
- const role = turn.role === "assistant" ? "assistant" : turn.role === "user" ? "user" : "system";
6314
- lines.push(`${role}: ${clip(body, PROMPT_TURN_CHAR_MAX)}`);
6315
- }
6316
- return lines.join("\n\n");
6317
- }
6318
- function textBodyOf(turn) {
6319
- const parts = [];
6320
- for (const block of turn.content) if (block.type === "text" && block.text.trim()) parts.push(block.text.trim());
6321
- else if (block.type === "tool_call") parts.push(`[tool: ${block.name}]`);
6322
- else if (block.type === "tool_result") parts.push(`[tool result]`);
6323
- return parts.join(" ");
6324
- }
6325
- /**
6326
- * Normalize a model-generated title into the shape we want to persist:
6327
- *
6328
- * - Collapse internal whitespace + take the first line only (some
6329
- * models emit "Title: foo\n\nReason: …" despite instructions).
6330
- * - Strip surrounding quote characters (`"foo"`, `'foo'`, `` `foo` ``).
6331
- * - Strip a leading `Title:` / `title -` prefix if the model added one.
6332
- * - Strip trailing punctuation (`.`, `!`, `?`) — titles read cleaner
6333
- * without it.
6334
- * - Truncate to `TITLE_MAX_CHARS` with a trailing `…` when over.
6335
- *
6336
- * Exported as `cleanTitle` so tests can pin the normalization rules
6337
- * without going through a mock provider.
6338
- */
6339
- function cleanTitle(raw) {
6340
- let s = raw.split("\n")[0]?.trim() ?? "";
6341
- s = s.replace(/^\s*title\s*[:\-–—]\s*/i, "");
6342
- if (s.length >= 2) {
6343
- const first = s.charAt(0);
6344
- const last = s.charAt(s.length - 1);
6345
- if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`" || first === "“" && last === "”") s = s.slice(1, -1).trim();
6346
- }
6347
- s = s.replace(/[.!?]+$/, "").trim();
6348
- if (s.length === 0) throw new Error("Model returned an empty title.");
6349
- return s.length > TITLE_MAX_CHARS ? `${s.slice(0, TITLE_MAX_CHARS - 1)}…` : s;
6350
- }
6351
- function clip(text, max) {
6352
- return text.length > max ? `${text.slice(0, max)}…` : text;
6353
- }
6354
- //#endregion
6355
5783
  //#region src/chat/hints.ts
6356
5784
  /**
6357
5785
  * Truncate `text` to at most `max` characters, replacing the trailing
@@ -6890,181 +6318,6 @@ function makeRequestInteraction(actions) {
6890
6318
  });
6891
6319
  }
6892
6320
  //#endregion
6893
- //#region src/chat/markdown-segments.ts
6894
- /**
6895
- * Split markdown text into alternating prose / fenced-code segments.
6896
- *
6897
- * Recognizes both ` ``` ` and `~~~` fences, with arbitrary length ≥ 3,
6898
- * matching CommonMark's "fence indicator must be at least as long to
6899
- * close" rule. Info-strings (the bit after the opening fence) are kept
6900
- * as the `lang` hint; any trailing whitespace is trimmed. Closing
6901
- * fences are detected on a line of their own (whitespace tolerated).
6902
- *
6903
- * Unclosed fences fall back to emitting the would-be-code body as a
6904
- * trailing prose segment so no content is dropped — finalized markdown
6905
- * isn't expected to ship one, but the fallback keeps the renderer
6906
- * truthful when the model produces malformed output.
6907
- *
6908
- * Exported for unit tests.
6909
- */
6910
- function splitMarkdownCodeBlocks(text) {
6911
- const lines = text.split("\n");
6912
- const segments = [];
6913
- let prose = [];
6914
- let code = [];
6915
- let inFence = false;
6916
- let fenceChar = "";
6917
- let fenceLen = 0;
6918
- let lang = "";
6919
- const flushProse = () => {
6920
- if (prose.length > 0 && prose[prose.length - 1] === "") prose.pop();
6921
- if (prose.length > 0) {
6922
- segments.push({
6923
- kind: "prose",
6924
- content: prose.join("\n")
6925
- });
6926
- prose = [];
6927
- }
6928
- };
6929
- let trimLeadingProseBlank = false;
6930
- for (const line of lines) if (!inFence) {
6931
- const open = line.match(/^(`{3,}|~{3,})([^\n`~]*)$/);
6932
- if (open) {
6933
- flushProse();
6934
- inFence = true;
6935
- fenceChar = open[1][0];
6936
- fenceLen = open[1].length;
6937
- lang = open[2].trim();
6938
- code = [];
6939
- continue;
6940
- }
6941
- if (trimLeadingProseBlank) {
6942
- trimLeadingProseBlank = false;
6943
- if (line === "" && prose.length === 0) continue;
6944
- }
6945
- prose.push(line);
6946
- } else {
6947
- const close = line.match(/^(`{3,}|~{3,})\s*$/);
6948
- if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
6949
- segments.push({
6950
- kind: "code",
6951
- content: code.join("\n"),
6952
- lang
6953
- });
6954
- inFence = false;
6955
- code = [];
6956
- trimLeadingProseBlank = true;
6957
- continue;
6958
- }
6959
- code.push(line);
6960
- }
6961
- if (inFence) prose.push(`${fenceChar.repeat(fenceLen)}${lang}`, ...code);
6962
- flushProse();
6963
- return segments;
6964
- }
6965
- const LIST_ITEM_RE = /^( *)(?:[-+*]|\d+[.)]) +/;
6966
- /**
6967
- * Lift indented fenced code blocks to column 0 while preserving the code
6968
- * body's relative indentation.
6969
- *
6970
- * OpenTUI's markdown renderNode hook only receives top-level code tokens.
6971
- * A common assistant shape is:
6972
- *
6973
- * - Run this:
6974
- * ```bash
6975
- * command
6976
- * ```
6977
- *
6978
- * Marked parses that as code inside a list token, so the TUI loses the
6979
- * code-panel wrapper and copy affordance. Normalizing just the fence's list
6980
- * indentation keeps the prose readable while routing the snippet through
6981
- * the same top-level renderer as ordinary fences.
6982
- */
6983
- function normalizeMarkdownCodeFences(text) {
6984
- const lines = text.split("\n");
6985
- const out = [];
6986
- const listIndents = [];
6987
- let inFence = false;
6988
- let fenceChar = "";
6989
- let fenceLen = 0;
6990
- let lift = 0;
6991
- for (const line of lines) {
6992
- if (inFence) {
6993
- const normalized = stripFenceIndent(line, lift);
6994
- out.push(normalized);
6995
- if (isFenceClose(normalized, fenceChar, fenceLen)) {
6996
- inFence = false;
6997
- fenceChar = "";
6998
- fenceLen = 0;
6999
- lift = 0;
7000
- }
7001
- continue;
7002
- }
7003
- updateListIndentStack(listIndents, line);
7004
- const open = parseFenceOpen(line);
7005
- if (!open) {
7006
- out.push(line);
7007
- continue;
7008
- }
7009
- lift = [...listIndents].reverse().find((i) => i <= open.indent) ?? (open.indent <= 3 ? open.indent : 0);
7010
- const normalized = stripFenceIndent(line, lift);
7011
- out.push(normalized);
7012
- inFence = true;
7013
- fenceChar = open.char;
7014
- fenceLen = open.length;
7015
- }
7016
- return out.join("\n");
7017
- }
7018
- function parseFenceOpen(line) {
7019
- const indent = leadingSpaces(line);
7020
- const char = line[indent];
7021
- if (char !== "`" && char !== "~") return null;
7022
- const length = countRun(line, indent, char);
7023
- if (length < 3) return null;
7024
- const info = line.slice(indent + length);
7025
- if (char === "`" && info.includes("`")) return null;
7026
- return {
7027
- indent,
7028
- char,
7029
- length
7030
- };
7031
- }
7032
- function isFenceClose(line, fenceChar, fenceLen) {
7033
- const indent = leadingSpaces(line);
7034
- if (line[indent] !== fenceChar) return false;
7035
- const length = countRun(line, indent, fenceChar);
7036
- if (length < fenceLen) return false;
7037
- return onlySpaces(line, indent + length);
7038
- }
7039
- function updateListIndentStack(listIndents, line) {
7040
- const contentIndent = leadingSpaces(line);
7041
- if (line.trim().length > 0) while (listIndents.length > 0 && contentIndent < listIndents[listIndents.length - 1]) listIndents.pop();
7042
- const item = line.match(LIST_ITEM_RE);
7043
- if (!item) return;
7044
- const indent = item[0].length;
7045
- if (indent > (listIndents[listIndents.length - 1] ?? 0)) listIndents.push(indent);
7046
- }
7047
- function leadingSpaces(line) {
7048
- let i = 0;
7049
- while (i < line.length && line[i] === " ") i++;
7050
- return i;
7051
- }
7052
- function countRun(line, start, char) {
7053
- let i = start;
7054
- while (i < line.length && line[i] === char) i++;
7055
- return i - start;
7056
- }
7057
- function onlySpaces(line, start) {
7058
- for (let i = start; i < line.length; i++) if (line[i] !== " ") return false;
7059
- return true;
7060
- }
7061
- function stripFenceIndent(line, columns) {
7062
- if (columns <= 0) return line;
7063
- let i = 0;
7064
- while (i < columns && line[i] === " ") i++;
7065
- return line.slice(i);
7066
- }
7067
- //#endregion
7068
6321
  //#region src/chat/mcp-auth-state.ts
7069
6322
  /**
7070
6323
  * Apply one event to the state map. Pure, immutable — returns a new map
@@ -7527,58 +6780,6 @@ function isCachedEntry(value) {
7527
6780
  return true;
7528
6781
  }
7529
6782
  //#endregion
7530
- //#region src/chat/project-user-paths.ts
7531
- /**
7532
- * Shared search-path builder for project + user config discovery.
7533
- *
7534
- * Both Skills (`skills/<name>/SKILL.md`) and MCP servers (`mcps.json`)
7535
- * follow the same first-found-wins order:
7536
- *
7537
- * 1. `{project-root}/.agents/<sub>` — project, agnostic convention
7538
- * 2. `{project-root}/.{prefix}/<sub>` — project, zidane (`.zidane` default)
7539
- * 3. `~/.agents/<sub>` — user, agnostic
7540
- * 4. `~/.{prefix}/<sub>` — user, zidane
7541
- *
7542
- * `{project-root}` is the git root walked up from `cwd` when present,
7543
- * else `cwd` itself. This pairs with the project-scoped session
7544
- * storage (`resolveStoragePaths`) so all three project-aware surfaces
7545
- * — sessions, mcps, skills — anchor at the same `.{prefix}/`.
7546
- *
7547
- * Centralizing the pattern keeps the four paths in one place — a third
7548
- * discovery surface (hooks, rules, …) gets the same convention for free
7549
- * and stays in lock-step on a prefix bump.
7550
- */
7551
- /**
7552
- * Resolve the four search paths for a given subdirectory (`skills`,
7553
- * `mcps.json`, …). Returns absolute paths regardless of whether they
7554
- * exist — the caller chooses to `existsSync`-filter or feed straight
7555
- * into a tolerant discovery routine.
7556
- */
7557
- function projectUserPaths(opts) {
7558
- const cwd = opts.cwd ?? process.cwd();
7559
- const home = opts.home ?? homedir();
7560
- const projectRoot = findGitRoot$1(cwd) ?? cwd;
7561
- const prefix = (opts.prefix ?? ".zidane").replace(/^\./, "");
7562
- return [
7563
- {
7564
- path: resolve(projectRoot, `.agents/${opts.subPath}`),
7565
- source: "project"
7566
- },
7567
- {
7568
- path: resolve(projectRoot, `.${prefix}/${opts.subPath}`),
7569
- source: "project"
7570
- },
7571
- {
7572
- path: resolve(home, `.agents/${opts.subPath}`),
7573
- source: "user"
7574
- },
7575
- {
7576
- path: resolve(home, `.${prefix}/${opts.subPath}`),
7577
- source: "user"
7578
- }
7579
- ];
7580
- }
7581
- //#endregion
7582
6783
  //#region src/chat/mcps-discovery.ts
7583
6784
  /**
7584
6785
  * File names searched in each of the four project/user roots. Order
@@ -9060,6 +8261,9 @@ function useStreamBuffer(setEvents, options) {
9060
8261
  const lastTickMsRef = useRef(0);
9061
8262
  const getSmoothRef = useRef(options?.getSmooth);
9062
8263
  getSmoothRef.current = options?.getSmooth;
8264
+ /** Live mirror so drains read the current cap without re-memoizing. */
8265
+ const maxEventsRef = useRef(options?.maxTranscriptEvents);
8266
+ maxEventsRef.current = options?.maxTranscriptEvents;
9063
8267
  /**
9064
8268
  * Updaters queued while smooth-streaming was still draining backlog —
9065
8269
  * tool-call appends, finalize-markdown transforms, error events.
@@ -9111,7 +8315,8 @@ function useStreamBuffer(setEvents, options) {
9111
8315
  for (const bucket of buckets) merged = applyBucket(merged, bucket);
9112
8316
  for (const u of queued) merged = u(merged);
9113
8317
  if (updater) merged = updater(merged);
9114
- return merged;
8318
+ const cap = maxEventsRef.current;
8319
+ return cap === null ? merged : capTranscriptEvents(merged, cap ?? void 0);
9115
8320
  });
9116
8321
  }, [setEvents, stopTicker]);
9117
8322
  /**
@@ -9309,6 +8514,6 @@ function computeTurnAnchors(items) {
9309
8514
  };
9310
8515
  }
9311
8516
  //#endregion
9312
- export { useMcpAuthDispatch as $, useActiveTodos as $n, resolveStoragePaths as $t, runOAuthLogin as A, findGitRoot$1 as An, toolResultText as Ar, SETTINGS_INPUTS as At, refreshMcpToolsCatalog as B, TODOREAD_TOOL as Bn, GRUVBOX_DARK as Bt, projectsFilePath as C, parseSemver as Cn, marginTopFor as Cr, buildHints as Ct, formatPathForCwd as D, chatAutoCompactBehavior as Dn, sumRunCosts as Dr, DEFAULT_SETTINGS as Dt, writeProjects as E, resolvePlatformPackage as En, stripSpawnTokensLine as Er, useEnabledToggleSet as Et, parseMcpsFile as F, DEFAULT_PERSIST_EXCLUDE_TOOLS as Fn, BUILTIN_THEMES as Ft, useMcpToolToggleSet as G, createTodoTools as Gn, CATPPUCCIN_MOCHA as Gt, subscribeMcpToolsCache as H, TODOWRITE_TOOL as Hn, CATPPUCCIN_FRAPPE as Ht, projectUserPaths as I, PLAN_AGENT as In, DEFAULT_THEME as It, parentServerName as J, isTodoTool as Jn, useDiscovery as Jt, buildVisibleMcpRows as K, getArchivedTodosForRun as Kn, createDiscoverySlot as Kt, clearMcpToolsCache as L, accentColor as Ln, resolveChipColor as Lt, buildMcpServers as M, BUILTIN_AGENTS as Mn, SettingsProvider as Mt, defaultMcpsConfigPaths as N, DEFAULT_AGENT_ID as Nn, clampFps as Nt, fetchOAuthRedirect as O, discoverAgentsMd as On, titleFromTurns as Or, SETTINGS_CATEGORIES as Ot, discoverProjectMcps as P, DEFAULT_BUDGET_EXCLUDE_TOOLS as Pn, useSettings as Pt, McpAuthProvider as Q, setTodosForRun as Qn, resolveConfig as Qt, loadMcpToolsCache as R, resolveAgentId as Rn, resolveTheme as Rt, matchesSafelistEntry as S, detectPackageManager as Sn, loadState as Sr, shouldAutoGenerateTitle as St, suggestSafelistEntries as T, performSelfUpdate as Tn, serverToolResultSummary as Tr, listProjectFiles as Tt, buildToolToggle as U, TODO_STATUS_GLYPHS as Un, CATPPUCCIN_LATTE as Ut, saveMcpToolsCache as V, TODOS_METADATA_KEY as Vn, GRUVBOX_LIGHT as Vt, useMcpToolToggleMap as W, TODO_WRITE_COUNTS_METADATA_KEY as Wn, CATPPUCCIN_MACCHIATO as Wt, mcpCredentialsPath as X, pruneTodosByRun as Xn, ConfigProvider as Xt, createFileMcpCredentialStore as Y, pickActiveRunId as Yn, useDiscoveryOptional as Yt, patchMcpCredential as Z, selectActiveTodos as Zn, useConfig as Zt, useSafeModeQueue as _, buildUpdateHint as _n, createStateStore as _r, clipHintsToWidth as _t, useSurfaces as a, formatBindingForDisplay as an, INTERACTION_GUIDANCE_NO_PROMPTS as ar, ASK_USER_TOOL as at, getSafelist as b, compareSemver as bn, lastContextSizeFromTurns as br, cleanTitle as bt, useStreamBuffer as c, matchesBinding as cn, SUBAGENT_GUIDANCE as cr, buildResumedToolResultsTurn as ct, discoverProjectSkills as d, readKeybindings as dn, buildPlanSystem as dr, makeRequestInteraction as dt, DEFAULT_KEYBINDINGS as en, ACTIONS_WITH_CARE_DOCTRINE as er, useMcpAuthState as et, renderSession as f, stripJsonComments as fn, envSection as fr, pendingInteractionsFromTurns as ft, useSafeModeActions as g, bootTick as gn, compactSummaryEvent as gr, EMPTY_HINTS as gt, SafeModeProvider as h, bootProfileEnabled as hn, DEFAULT_SESSION_TITLE as hr, useInteractionsQueue as ht, useSelectStyle as i, ensureKeybindingsFile as in, INTERACTION_GUIDANCE as ir, splitMarkdownCodeBlocks as it, supportsOAuth as j, BUILD_AGENT as jn, updateToolEventOutcomes as jr, SETTINGS_TOGGLES as jt, oauthUsesManualCodePaste as k, renderAgentsMdBlock as kn, toolCallPreview as kr, SETTINGS_CHOICES as kt, buildSkillsConfig as l, mergeKeybindings as ln, TOKEN_DISCIPLINE_DOCTRINE as lr, createInteractionTools as lt, writeSessionExport as m, tryOpenBrowser as mn, createChatTurnsReadTool as mr, useInteractionsActions as mt, ThemeProvider as n, KEYBINDING_DEF_BY_ACTION as nn, DOING_TASKS_DOCTRINE as nr, reduceMcpAuth as nt, useSyntaxStyles as o, groupBindings as on, PLAN_MODE_DOCTRINE as or, InteractionsProvider as ot, resolveSessionExportTarget as p, useCompletion as pn, chatReferenceTokensForPrompt as pr, serializeInteractionResponse as pt, indexOfServerRow as q, getTodosForRun as qn, DiscoveryProvider as qt, useColors as r, KEYBINDING_KEY_COL_WIDTH as rn, IDENTITY_PREFIX as rr, normalizeMarkdownCodeFences as rt, useTheme as s, keybindingsPath as sn, PLAN_MODE_DOCTRINE_NO_PROMPTS as sr, PRESENT_PLAN_TOOL as st, computeTurnAnchors as t, KEYBINDING_DEFS as tn, COMMUNICATION_DOCTRINE as tr, getMcpAuthStatus as tt, defaultSkillScanPaths as u, parseBindingSpec as un, buildBuildSystem as ur, isInteractionTool as ut, IMPLICITLY_SAFE_TOOLS as v, useUpdateCheck as vn, deriveSessionTitle as vr, hintsLength as vt, readProjects as w, performInPlaceSelfUpdate as wn, saveState as wr, shortChord as wt, isOnSafelist as x, detectLibc as xn, listSessionMeta as xr, generateSessionTitle as xt, addToSafelist as y, checkForUpdate as yn, eventsFromTurns as yr, truncateTrailing as yt, mcpToolsCachePath as z, singleAgentRegistry as zn, VAPORWAVE_THEME as zt };
8517
+ export { useMcpAuthState as $, eventsFromTurns as $n, useUpdateCheck as $t, runOAuthLogin as A, pruneTodosByRun as An, DEFAULT_THEME as At, saveMcpToolsCache as B, PLAN_MODE_DOCTRINE as Bn, createDiscoverySlot as Bt, projectsFilePath as C, TODO_STATUS_GLYPHS as Cn, SETTINGS_CHOICES as Ct, formatPathForCwd as D, getTodosForRun as Dn, clampFps as Dt, writeProjects as E, getArchivedTodosForRun as En, SettingsProvider as Et, parseMcpsFile as F, COMMUNICATION_DOCTRINE as Fn, GRUVBOX_LIGHT as Ft, buildVisibleMcpRows as G, buildPlanSystem as Gn, useConfig as Gt, buildToolToggle as H, SUBAGENT_GUIDANCE as Hn, useDiscovery as Ht, clearMcpToolsCache as I, DOING_TASKS_DOCTRINE as In, CATPPUCCIN_FRAPPE as It, createFileMcpCredentialStore as J, createChatTurnsReadTool as Jn, useCompletion as Jt, indexOfServerRow as K, envSection as Kn, resolveConfig as Kt, loadMcpToolsCache as L, IDENTITY_PREFIX as Ln, CATPPUCCIN_LATTE as Lt, buildMcpServers as M, setTodosForRun as Mn, resolveTheme as Mt, defaultMcpsConfigPaths as N, useActiveTodos as Nn, VAPORWAVE_THEME as Nt, fetchOAuthRedirect as O, isTodoTool as On, useSettings as Ot, discoverProjectMcps as P, ACTIONS_WITH_CARE_DOCTRINE as Pn, GRUVBOX_DARK as Pt, useMcpAuthDispatch as Q, deriveSessionTitle as Qn, buildUpdateHint as Qt, mcpToolsCachePath as R, INTERACTION_GUIDANCE as Rn, CATPPUCCIN_MACCHIATO as Rt, matchesSafelistEntry as S, TODOWRITE_TOOL as Sn, SETTINGS_CATEGORIES as St, suggestSafelistEntries as T, createTodoTools as Tn, SETTINGS_TOGGLES as Tt, useMcpToolToggleMap as U, TOKEN_DISCIPLINE_DOCTRINE as Un, useDiscoveryOptional as Ut, subscribeMcpToolsCache as V, PLAN_MODE_DOCTRINE_NO_PROMPTS as Vn, DiscoveryProvider as Vt, useMcpToolToggleSet as W, buildBuildSystem as Wn, ConfigProvider as Wt, patchMcpCredential as X, compactSummaryEvent as Xn, bootProfileEnabled as Xt, mcpCredentialsPath as Y, DEFAULT_SESSION_TITLE as Yn, tryOpenBrowser as Yt, McpAuthProvider as Z, createStateStore as Zn, bootTick as Zt, useSafeModeQueue as _, accentColor as _n, buildHints as _t, useSurfaces as a, performInPlaceSelfUpdate as an, serverToolResultSummary as ar, buildResumedToolResultsTurn as at, getSafelist as b, TODOREAD_TOOL as bn, useEnabledToggleSet as bt, useStreamBuffer as c, chatAutoCompactBehavior as cn, titleFromTurns as cr, makeRequestInteraction as ct, discoverProjectSkills as d, BUILD_AGENT as dn, updateToolEventOutcomes as dr, useInteractionsActions as dt, checkForUpdate as en, lastContextSizeFromTurns as er, getMcpAuthStatus as et, renderSession as f, BUILTIN_AGENTS as fn, useInteractionsQueue as ft, useSafeModeActions as g, PLAN_AGENT as gn, truncateTrailing as gt, SafeModeProvider as h, DEFAULT_PERSIST_EXCLUDE_TOOLS as hn, hintsLength as ht, useSelectStyle as i, parseSemver as in, saveState as ir, PRESENT_PLAN_TOOL as it, supportsOAuth as j, selectActiveTodos as jn, resolveChipColor as jt, oauthUsesManualCodePaste as k, pickActiveRunId as kn, BUILTIN_THEMES as kt, buildSkillsConfig as l, discoverAgentsMd as ln, toolCallPreview as lr, pendingInteractionsFromTurns as lt, writeSessionExport as m, DEFAULT_BUDGET_EXCLUDE_TOOLS as mn, clipHintsToWidth as mt, ThemeProvider as n, detectLibc as nn, loadState as nr, ASK_USER_TOOL as nt, useSyntaxStyles as o, performSelfUpdate as on, stripSpawnTokensLine as or, createInteractionTools as ot, resolveSessionExportTarget as p, DEFAULT_AGENT_ID as pn, EMPTY_HINTS as pt, parentServerName as q, chatReferenceTokensForPrompt as qn, resolveStoragePaths as qt, useColors as r, detectPackageManager as rn, marginTopFor as rr, InteractionsProvider as rt, useTheme as s, resolvePlatformPackage as sn, sumRunCosts as sr, isInteractionTool as st, computeTurnAnchors as t, compareSemver as tn, listSessionMeta as tr, reduceMcpAuth as tt, defaultSkillScanPaths as u, renderAgentsMdBlock as un, toolResultText as ur, serializeInteractionResponse as ut, IMPLICITLY_SAFE_TOOLS as v, resolveAgentId as vn, shortChord as vt, readProjects as w, TODO_WRITE_COUNTS_METADATA_KEY as wn, SETTINGS_INPUTS as wt, isOnSafelist as x, TODOS_METADATA_KEY as xn, DEFAULT_SETTINGS as xt, addToSafelist as y, singleAgentRegistry as yn, listProjectFiles as yt, refreshMcpToolsCatalog as z, INTERACTION_GUIDANCE_NO_PROMPTS as zn, CATPPUCCIN_MOCHA as zt };
9313
8518
 
9314
- //# sourceMappingURL=transcript-anchors-DR4URysA.js.map
8519
+ //# sourceMappingURL=transcript-anchors-DSRPWjiH.js.map