zidane 5.1.13 → 5.1.15

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 (65) hide show
  1. package/dist/{agent-skiQGYs2.d.ts → agent-a-mteIEP.d.ts} +19 -4
  2. package/dist/agent-a-mteIEP.d.ts.map +1 -0
  3. package/dist/chat.d.ts +172 -7
  4. package/dist/chat.d.ts.map +1 -1
  5. package/dist/chat.js +2 -2
  6. package/dist/{errors-D1lhd6mX.js → errors-COmsomd5.js} +13 -3
  7. package/dist/{errors-D1lhd6mX.js.map → errors-COmsomd5.js.map} +1 -1
  8. package/dist/{index-YM7SipFz.d.ts → index-CsdPEjlu.d.ts} +2 -2
  9. package/dist/{index-YM7SipFz.d.ts.map → index-CsdPEjlu.d.ts.map} +1 -1
  10. package/dist/{index-CjPh6CRE.d.ts → index-D5gCRi42.d.ts} +2 -2
  11. package/dist/{index-CjPh6CRE.d.ts.map → index-D5gCRi42.d.ts.map} +1 -1
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +10 -10
  14. package/dist/{interpolate-BI6ovwag.js → interpolate-BhmHKD6x.js} +3 -4
  15. package/dist/{interpolate-BI6ovwag.js.map → interpolate-BhmHKD6x.js.map} +1 -1
  16. package/dist/{login-Cc6Q-Fpu.js → login-DrnEZZVv.js} +4 -4
  17. package/dist/{login-Cc6Q-Fpu.js.map → login-DrnEZZVv.js.map} +1 -1
  18. package/dist/{mcp-CUt-N8zn.js → mcp-B1psg7jf.js} +4 -4
  19. package/dist/mcp-B1psg7jf.js.map +1 -0
  20. package/dist/mcp.d.ts +1 -1
  21. package/dist/mcp.js +1 -1
  22. package/dist/{messages-CIkO_aCH.js → messages-DsbMYNmt.js} +26 -34
  23. package/dist/messages-DsbMYNmt.js.map +1 -0
  24. package/dist/{presets-Ce79MK4J.js → presets-H8UYtz3b.js} +2 -2
  25. package/dist/{presets-Ce79MK4J.js.map → presets-H8UYtz3b.js.map} +1 -1
  26. package/dist/presets.d.ts +2 -2
  27. package/dist/presets.js +1 -1
  28. package/dist/{providers-CvriFHFU.js → providers-v1Rn2rqG.js} +40 -14
  29. package/dist/providers-v1Rn2rqG.js.map +1 -0
  30. package/dist/providers.d.ts +1 -1
  31. package/dist/providers.js +2 -2
  32. package/dist/session/sqlite.d.ts +1 -1
  33. package/dist/session/sqlite.d.ts.map +1 -1
  34. package/dist/session/sqlite.js +2 -2
  35. package/dist/session/sqlite.js.map +1 -1
  36. package/dist/{session-DtLD1Sl1.js → session-DOJgRXvF.js} +2 -2
  37. package/dist/{session-DtLD1Sl1.js.map → session-DOJgRXvF.js.map} +1 -1
  38. package/dist/session.d.ts +1 -1
  39. package/dist/session.js +2 -2
  40. package/dist/skills.d.ts +2 -2
  41. package/dist/skills.js +1 -1
  42. package/dist/{tools-BG2wMa3X.js → tools-Duptt9yy.js} +16 -20
  43. package/dist/tools-Duptt9yy.js.map +1 -0
  44. package/dist/tools.d.ts +2 -2
  45. package/dist/tools.js +1 -1
  46. package/dist/{tool-formatters-0aOMYbH-.d.ts → transcript-anchors-YFom211q.d.ts} +242 -97
  47. package/dist/transcript-anchors-YFom211q.d.ts.map +1 -0
  48. package/dist/tui.d.ts +55 -39
  49. package/dist/tui.d.ts.map +1 -1
  50. package/dist/tui.js +347 -326
  51. package/dist/tui.js.map +1 -1
  52. package/dist/{turn-operations-CDmQ2h-T.js → turn-operations-DNKpDGQi.js} +600 -158
  53. package/dist/turn-operations-DNKpDGQi.js.map +1 -0
  54. package/dist/{types-Bx_F8jet.js → types-IcokUOyC.js} +11 -4
  55. package/dist/{types-Bx_F8jet.js.map → types-IcokUOyC.js.map} +1 -1
  56. package/dist/types.d.ts +2 -2
  57. package/dist/types.js +2 -2
  58. package/package.json +1 -1
  59. package/dist/agent-skiQGYs2.d.ts.map +0 -1
  60. package/dist/mcp-CUt-N8zn.js.map +0 -1
  61. package/dist/messages-CIkO_aCH.js.map +0 -1
  62. package/dist/providers-CvriFHFU.js.map +0 -1
  63. package/dist/tool-formatters-0aOMYbH-.d.ts.map +0 -1
  64. package/dist/tools-BG2wMa3X.js.map +0 -1
  65. package/dist/turn-operations-CDmQ2h-T.js.map +0 -1
@@ -1,19 +1,21 @@
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-BG2wMa3X.js";
2
- import { n as toolResultToText } from "./types-Bx_F8jet.js";
3
- import { r as normalizeMcpServers } from "./mcp-CUt-N8zn.js";
4
- import { a as discoverSkills } from "./interpolate-BI6ovwag.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-Duptt9yy.js";
2
+ import { o as errorMessage } from "./errors-COmsomd5.js";
3
+ import { n as toolResultToText } from "./types-IcokUOyC.js";
4
+ import { r as normalizeMcpServers } from "./mcp-B1psg7jf.js";
5
+ import { a as discoverSkills } from "./interpolate-BhmHKD6x.js";
5
6
  import { n as formatTokenUsage } from "./stats-DgOvY7wd.js";
6
- import { n as definePreset } from "./presets-Ce79MK4J.js";
7
- import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-CvriFHFU.js";
7
+ import { n as definePreset } from "./presets-H8UYtz3b.js";
8
+ import { a as writeFileAtomic, i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-v1Rn2rqG.js";
8
9
  import { spawn } from "node:child_process";
9
10
  import { readdir, stat, writeFile } from "node:fs/promises";
10
11
  import { dirname, join, resolve, sep } from "node:path";
11
12
  import { getModel, getModels } from "@mariozechner/pi-ai";
12
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
13
+ import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
13
14
  import { homedir } from "node:os";
14
15
  import { anthropicOAuthProvider, openaiCodexOAuthProvider } from "@mariozechner/pi-ai/oauth";
15
16
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
16
17
  import { jsx } from "react/jsx-runtime";
18
+ import { jsx as jsx$1 } from "@opentui/react/jsx-runtime";
17
19
  //#region src/chat/agent-prompt.ts
18
20
  /**
19
21
  * Agent system-prompt fragments — composable doctrine for built-in profiles.
@@ -618,11 +620,10 @@ function readProviderCredential(dataDir, descriptor) {
618
620
  * (first launch on a fresh machine: `~/.zidane/` may not exist yet).
619
621
  */
620
622
  function writeCredentials(dataDir, creds) {
621
- const path = credentialsPath(dataDir);
622
- mkdirSync(dirname(path), { recursive: true });
623
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
624
- writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: FILE_MODE$1 });
625
- renameSync(tmp, path);
623
+ writeFileAtomic(credentialsPath(dataDir), `${JSON.stringify(creds, null, 2)}\n`, {
624
+ ensureDir: true,
625
+ mode: FILE_MODE$1
626
+ });
626
627
  }
627
628
  function setProviderCredential(dataDir, descriptor, cred) {
628
629
  const all = readCredentials(dataDir);
@@ -729,10 +730,10 @@ function migrateLegacyFile(targetPath) {
729
730
  };
730
731
  }
731
732
  if (Object.keys(migrated).length === 0) return null;
732
- mkdirSync(dirname(targetPath), { recursive: true });
733
- const tmp = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
734
- writeFileSync(tmp, `${JSON.stringify(migrated, null, 2)}\n`, { mode: FILE_MODE$1 });
735
- renameSync(tmp, targetPath);
733
+ writeFileAtomic(targetPath, `${JSON.stringify(migrated, null, 2)}\n`, {
734
+ ensureDir: true,
735
+ mode: FILE_MODE$1
736
+ });
736
737
  return migrated;
737
738
  }
738
739
  function isOAuthLegacy(value) {
@@ -848,6 +849,43 @@ function shouldAutoCompact(input) {
848
849
  };
849
850
  }
850
851
  //#endregion
852
+ //#region src/chat/boot-profiler.ts
853
+ const enabled = !!process.env.ZIDANE_BOOT_PROFILE;
854
+ /**
855
+ * High-resolution origin for the running profile. Captured at module
856
+ * load time so the first {@link bootTick} call inside the same process
857
+ * reflects "time since the boot profiler was first reached", which for
858
+ * the standard `cli.ts → runTui` path is effectively "time since the
859
+ * TUI binary started executing user code".
860
+ */
861
+ const start = performance.now();
862
+ let last = start;
863
+ /**
864
+ * Record a checkpoint. No-op unless `ZIDANE_BOOT_PROFILE` is truthy in
865
+ * the environment.
866
+ *
867
+ * The leading delta is the time since the PREVIOUS tick (so a long
868
+ * delta highlights the immediately-preceding work); the trailing total
869
+ * is the time since the profiler started. Both round to one decimal of
870
+ * a millisecond.
871
+ */
872
+ function bootTick(label) {
873
+ if (!enabled) return;
874
+ const now = performance.now();
875
+ const delta = now - last;
876
+ const total = now - start;
877
+ last = now;
878
+ process.stderr.write(`[boot] +${delta.toFixed(1).padStart(7)}ms / total ${total.toFixed(1).padStart(7)}ms / ${label}\n`);
879
+ }
880
+ /**
881
+ * Returns `true` when `ZIDANE_BOOT_PROFILE` is set. Useful for guarding
882
+ * heavier instrumentation (e.g. wrapping a costly call in a span) that
883
+ * you don't want paying its own cost in the default path.
884
+ */
885
+ function bootProfileEnabled() {
886
+ return enabled;
887
+ }
888
+ //#endregion
851
889
  //#region src/chat/browser.ts
852
890
  /**
853
891
  * Best-effort cross-platform browser open.
@@ -887,6 +925,95 @@ function tryOpenBrowser(url) {
887
925
  } catch {}
888
926
  }
889
927
  //#endregion
928
+ //#region src/chat/color-gradient.ts
929
+ /** Parse `#rrggbb` (case-insensitive) into `[r, g, b]` 0–255 integers. */
930
+ function parseHex(hex) {
931
+ const h = hex.replace("#", "");
932
+ return [
933
+ Number.parseInt(h.slice(0, 2), 16),
934
+ Number.parseInt(h.slice(2, 4), 16),
935
+ Number.parseInt(h.slice(4, 6), 16)
936
+ ];
937
+ }
938
+ /** Convert sRGB 0–255 → HSL 0–1. */
939
+ function rgbToHsl(r, g, b) {
940
+ r /= 255;
941
+ g /= 255;
942
+ b /= 255;
943
+ const max = Math.max(r, g, b);
944
+ const min = Math.min(r, g, b);
945
+ const l = (max + min) / 2;
946
+ if (max === min) return [
947
+ 0,
948
+ 0,
949
+ l
950
+ ];
951
+ const d = max - min;
952
+ const s = l > .5 ? d / (2 - max - min) : d / (max + min);
953
+ let h;
954
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
955
+ else if (max === g) h = (b - r) / d + 2;
956
+ else h = (r - g) / d + 4;
957
+ return [
958
+ h / 6,
959
+ s,
960
+ l
961
+ ];
962
+ }
963
+ /** Convert HSL 0–1 → sRGB 0–255. Standard piecewise formula. */
964
+ function hslToRgb(h, s, l) {
965
+ if (s === 0) return [
966
+ l * 255,
967
+ l * 255,
968
+ l * 255
969
+ ];
970
+ const hue2rgb = (p, q, t) => {
971
+ if (t < 0) t += 1;
972
+ if (t > 1) t -= 1;
973
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
974
+ if (t < 1 / 2) return q;
975
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
976
+ return p;
977
+ };
978
+ const q = l < .5 ? l * (1 + s) : l + s - l * s;
979
+ const p = 2 * l - q;
980
+ return [
981
+ hue2rgb(p, q, h + 1 / 3) * 255,
982
+ hue2rgb(p, q, h) * 255,
983
+ hue2rgb(p, q, h - 1 / 3) * 255
984
+ ];
985
+ }
986
+ function toHex(rgb) {
987
+ const pad = (v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, "0");
988
+ return `#${pad(rgb[0])}${pad(rgb[1])}${pad(rgb[2])}`;
989
+ }
990
+ /**
991
+ * Blend two hex colors in HSL space with shortest-path hue interpolation.
992
+ * `t` ∈ [0, 1]; `t=0` returns `from`, `t=1` returns `to`.
993
+ */
994
+ function blendHsl(from, to, t) {
995
+ const [r1, g1, b1] = parseHex(from);
996
+ const [r2, g2, b2] = parseHex(to);
997
+ const [h1, s1, l1] = rgbToHsl(r1, g1, b1);
998
+ const [h2, s2, l2] = rgbToHsl(r2, g2, b2);
999
+ let dh = h2 - h1;
1000
+ if (dh > .5) dh -= 1;
1001
+ else if (dh < -.5) dh += 1;
1002
+ return toHex(hslToRgb((h1 + dh * t + 1) % 1, s1 + (s2 - s1) * t, l1 + (l2 - l1) * t));
1003
+ }
1004
+ /**
1005
+ * Static gradient ramp of length `n` going from `from` (index 0) to
1006
+ * `to` (index n-1) in HSL space. For the cycling A→B→A→B ramp the
1007
+ * throbber uses, see `buildCycleRamp` in `src/tui/crush-throbber.tsx`.
1008
+ */
1009
+ function buildLinearRamp(from, to, n) {
1010
+ if (n <= 0) return [];
1011
+ if (n === 1) return [blendHsl(from, to, .5)];
1012
+ const ramp = [];
1013
+ for (let i = 0; i < n; i++) ramp.push(blendHsl(from, to, i / (n - 1)));
1014
+ return ramp;
1015
+ }
1016
+ //#endregion
890
1017
  //#region src/chat/completion.ts
891
1018
  /**
892
1019
  * Prompt autocompletion framework.
@@ -1117,6 +1244,66 @@ const FILES_TRIGGER = "@";
1117
1244
  /** Cap on returned items. Keeps the popover compact + render-cheap. */
1118
1245
  const DEFAULT_RESULT_LIMIT = 50;
1119
1246
  /**
1247
+ * Rank-and-slice a file catalog against a query. Hoisted to a module
1248
+ * helper so both the sync and async branches of `suggest()` share one
1249
+ * implementation (the async branch hits this once the lazy directory
1250
+ * walk resolves; sync branch hits it on every keystroke thereafter).
1251
+ */
1252
+ function scoreFiles(catalog, query, limit) {
1253
+ const q = query.trim().toLowerCase();
1254
+ const scored = [];
1255
+ for (const file of catalog) {
1256
+ const name = file.name.toLowerCase();
1257
+ const path = file.path.toLowerCase();
1258
+ if (q.length === 0) {
1259
+ scored.push({
1260
+ entry: file,
1261
+ rank: 4
1262
+ });
1263
+ continue;
1264
+ }
1265
+ if (name === q) {
1266
+ scored.push({
1267
+ entry: file,
1268
+ rank: 0
1269
+ });
1270
+ continue;
1271
+ }
1272
+ if (name.startsWith(q)) {
1273
+ scored.push({
1274
+ entry: file,
1275
+ rank: 1
1276
+ });
1277
+ continue;
1278
+ }
1279
+ if (name.includes(q)) {
1280
+ scored.push({
1281
+ entry: file,
1282
+ rank: 2
1283
+ });
1284
+ continue;
1285
+ }
1286
+ if (path.includes(q)) {
1287
+ scored.push({
1288
+ entry: file,
1289
+ rank: 3
1290
+ });
1291
+ continue;
1292
+ }
1293
+ }
1294
+ scored.sort((a, b) => {
1295
+ if (a.rank !== b.rank) return a.rank - b.rank;
1296
+ return a.entry.path.localeCompare(b.entry.path);
1297
+ });
1298
+ return scored.slice(0, limit).map(({ entry }) => ({
1299
+ id: entry.path,
1300
+ label: entry.name,
1301
+ description: parentDir(entry.path),
1302
+ insertText: `@${entry.path} `,
1303
+ data: entry
1304
+ }));
1305
+ }
1306
+ /**
1120
1307
  * Build an `@`-prefixed files completion provider against a *live* catalog.
1121
1308
  *
1122
1309
  * The factory captures a getter so the catalog can be re-scanned (cwd
@@ -1136,59 +1323,11 @@ function createFilesCompletionProvider(opts) {
1136
1323
  trigger: "@",
1137
1324
  label: "Files",
1138
1325
  suggest(query) {
1139
- const catalog = opts.getCatalog();
1140
- const q = query.trim().toLowerCase();
1141
- const scored = [];
1142
- for (const file of catalog) {
1143
- const name = file.name.toLowerCase();
1144
- const path = file.path.toLowerCase();
1145
- if (q.length === 0) {
1146
- scored.push({
1147
- entry: file,
1148
- rank: 4
1149
- });
1150
- continue;
1151
- }
1152
- if (name === q) {
1153
- scored.push({
1154
- entry: file,
1155
- rank: 0
1156
- });
1157
- continue;
1158
- }
1159
- if (name.startsWith(q)) {
1160
- scored.push({
1161
- entry: file,
1162
- rank: 1
1163
- });
1164
- continue;
1165
- }
1166
- if (name.includes(q)) {
1167
- scored.push({
1168
- entry: file,
1169
- rank: 2
1170
- });
1171
- continue;
1172
- }
1173
- if (path.includes(q)) {
1174
- scored.push({
1175
- entry: file,
1176
- rank: 3
1177
- });
1178
- continue;
1179
- }
1326
+ if (opts.ensureCatalog) {
1327
+ const pending = opts.ensureCatalog();
1328
+ if (opts.getCatalog().length === 0) return pending.then((loaded) => scoreFiles(loaded, query, limit));
1180
1329
  }
1181
- scored.sort((a, b) => {
1182
- if (a.rank !== b.rank) return a.rank - b.rank;
1183
- return a.entry.path.localeCompare(b.entry.path);
1184
- });
1185
- return scored.slice(0, limit).map(({ entry }) => ({
1186
- id: entry.path,
1187
- label: entry.name,
1188
- description: parentDir(entry.path),
1189
- insertText: `@${entry.path} `,
1190
- data: entry
1191
- }));
1330
+ return scoreFiles(opts.getCatalog(), query, limit);
1192
1331
  },
1193
1332
  parseReferences(text, _ctx) {
1194
1333
  const catalog = opts.getCatalog();
@@ -1196,9 +1335,7 @@ function createFilesCompletionProvider(opts) {
1196
1335
  const byPath = /* @__PURE__ */ new Map();
1197
1336
  for (const file of catalog) byPath.set(file.path, file);
1198
1337
  const refs = [];
1199
- const rx = /(^|\s)@(\S+)/g;
1200
- let m;
1201
- while ((m = rx.exec(text)) !== null) {
1338
+ for (const m of text.matchAll(/(^|\s)@(\S+)/g)) {
1202
1339
  const rawCandidate = m[2];
1203
1340
  const stripped = byPath.has(rawCandidate) ? rawCandidate : rawCandidate.replace(/[.,;:)\]}!?]+$/, "");
1204
1341
  const file = byPath.get(stripped);
@@ -1245,6 +1382,33 @@ const SKILLS_TRIGGER = "/";
1245
1382
  /** Valid skill-name shape (matches the parser): lowercase alnum + dashes. */
1246
1383
  const SKILL_NAME_RX = /^[a-z0-9][a-z0-9-]*$/;
1247
1384
  /**
1385
+ * Filter + rank visible skills against a query. Hoisted to a module
1386
+ * helper so the sync and async branches of `suggest()` share one
1387
+ * implementation (the async branch hits this once the lazy SKILL.md
1388
+ * scan resolves; sync branch hits it on every keystroke thereafter).
1389
+ */
1390
+ function scoreSkills(catalog, query) {
1391
+ const q = query.trim().toLowerCase();
1392
+ return catalog.filter((skill) => SKILL_NAME_RX.test(skill.name)).filter((skill) => {
1393
+ if (q.length === 0) return true;
1394
+ return skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q);
1395
+ }).sort((a, b) => {
1396
+ const an = a.name.toLowerCase();
1397
+ const bn = b.name.toLowerCase();
1398
+ if (q) {
1399
+ const aPrefix = an.startsWith(q);
1400
+ if (aPrefix !== bn.startsWith(q)) return aPrefix ? -1 : 1;
1401
+ }
1402
+ return an.localeCompare(bn);
1403
+ }).map((skill) => ({
1404
+ id: skill.name,
1405
+ label: skill.name,
1406
+ description: skill.description,
1407
+ insertText: `/${skill.name} `,
1408
+ data: skill
1409
+ }));
1410
+ }
1411
+ /**
1248
1412
  * Build a slash-command completion provider against a *live* skills
1249
1413
  * catalog. The factory captures a getter so the catalog can change across
1250
1414
  * renders (toggles, reload) without re-instantiating the provider.
@@ -1265,25 +1429,11 @@ function createSkillsCompletionProvider(opts) {
1265
1429
  trigger: "/",
1266
1430
  label: "Skills",
1267
1431
  suggest(query) {
1268
- const q = query.trim().toLowerCase();
1269
- return visible().filter((skill) => SKILL_NAME_RX.test(skill.name)).filter((skill) => {
1270
- if (q.length === 0) return true;
1271
- return skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q);
1272
- }).sort((a, b) => {
1273
- const an = a.name.toLowerCase();
1274
- const bn = b.name.toLowerCase();
1275
- if (q) {
1276
- const aPrefix = an.startsWith(q);
1277
- if (aPrefix !== bn.startsWith(q)) return aPrefix ? -1 : 1;
1278
- }
1279
- return an.localeCompare(bn);
1280
- }).map((skill) => ({
1281
- id: skill.name,
1282
- label: skill.name,
1283
- description: skill.description,
1284
- insertText: `/${skill.name} `,
1285
- data: skill
1286
- }));
1432
+ if (opts.ensureCatalog) {
1433
+ const pending = opts.ensureCatalog();
1434
+ if (opts.getCatalog().length === 0) return pending.then(() => scoreSkills(visible(), query));
1435
+ }
1436
+ return scoreSkills(visible(), query);
1287
1437
  },
1288
1438
  parseReferences(text, _ctx) {
1289
1439
  const catalog = visible();
@@ -1291,9 +1441,7 @@ function createSkillsCompletionProvider(opts) {
1291
1441
  const byName = /* @__PURE__ */ new Map();
1292
1442
  for (const skill of catalog) byName.set(skill.name, skill);
1293
1443
  const refs = [];
1294
- const rx = /(^|\s)(\/([a-z0-9][a-z0-9-]*))/g;
1295
- let m;
1296
- while ((m = rx.exec(text)) !== null) {
1444
+ for (const m of text.matchAll(/(^|\s)(\/([a-z0-9][a-z0-9-]*))/g)) {
1297
1445
  const name = m[3];
1298
1446
  const skill = byName.get(name);
1299
1447
  if (!skill) continue;
@@ -1623,11 +1771,7 @@ function readKeybindings(userDir) {
1623
1771
  function ensureKeybindingsFile(userDir) {
1624
1772
  const path = keybindingsPath(userDir);
1625
1773
  if (existsSync(path)) return path;
1626
- ensureDir$1(path);
1627
- const body = renderDefaultFile();
1628
- const tmp = `${path}.${process.pid}.tmp`;
1629
- writeFileSync(tmp, body);
1630
- renameSync(tmp, path);
1774
+ writeFileAtomic(path, renderDefaultFile(), { ensureDir: true });
1631
1775
  return path;
1632
1776
  }
1633
1777
  /**
@@ -1658,10 +1802,6 @@ function renderDefaultFile() {
1658
1802
  lines.push("");
1659
1803
  return lines.join("\n");
1660
1804
  }
1661
- function ensureDir$1(path) {
1662
- const dir = dirname(path);
1663
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1664
- }
1665
1805
  /**
1666
1806
  * Strip `//` line comments and `/* … *\/` block comments from a JSONC
1667
1807
  * source string. Stays out of string literals so a `"foo // bar"` value
@@ -1934,9 +2074,7 @@ function pushSegment(buf, seg) {
1934
2074
  function tokenize(s) {
1935
2075
  if (s === "") return [];
1936
2076
  const out = [];
1937
- const re = /\w+|\W+/g;
1938
- let match;
1939
- while ((match = re.exec(s)) !== null) out.push(match[0]);
2077
+ for (const match of s.matchAll(/\w+|\W+/g)) out.push(match[0]);
1940
2078
  return out;
1941
2079
  }
1942
2080
  /**
@@ -2074,8 +2212,7 @@ function ensureStateDir(path) {
2074
2212
  try {
2075
2213
  mkdirSync(dir, { recursive: true });
2076
2214
  } catch (err) {
2077
- const message = err instanceof Error ? err.message : String(err);
2078
- throw new Error(`Could not create TUI state directory at "${dir}". Override the location via \`runTui({ storageDir, prefix })\` or the \`ZIDANE_STORAGE_DIR\` env var. Original error: ${message}`);
2215
+ throw new Error(`Could not create TUI state directory at "${dir}". Override the location via \`runTui({ storageDir, prefix })\` or the \`ZIDANE_STORAGE_DIR\` env var. Original error: ${errorMessage(err)}`);
2079
2216
  }
2080
2217
  }
2081
2218
  function createStateStore(path) {
@@ -2094,9 +2231,7 @@ function loadState(path) {
2094
2231
  }
2095
2232
  function saveState(path, state) {
2096
2233
  ensureStateDir(path);
2097
- const tmp = `${path}.${process.pid}.tmp`;
2098
- writeFileSync(tmp, JSON.stringify(state, null, 2));
2099
- renameSync(tmp, path);
2234
+ writeFileAtomic(path, JSON.stringify(state, null, 2));
2100
2235
  }
2101
2236
  /**
2102
2237
  * Load every session and project it to the compact `SessionMeta` shape used by
@@ -2593,10 +2728,7 @@ function loadUserConfig(userDir) {
2593
2728
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
2594
2729
  return normalizeUserConfig(parsed);
2595
2730
  } catch (err) {
2596
- if (process.env.ZIDANE_DEBUG) {
2597
- const msg = err instanceof Error ? err.message : String(err);
2598
- process.stderr.write(`[zidane/chat] user-config: failed to read "${path}": ${msg}\n`);
2599
- }
2731
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] user-config: failed to read "${path}": ${errorMessage(err)}\n`);
2600
2732
  return {};
2601
2733
  }
2602
2734
  }
@@ -2686,10 +2818,7 @@ function resolveStoragePaths(opts) {
2686
2818
  if (projectDir && !existsSync(projectDir)) try {
2687
2819
  mkdirSync(projectDir, { recursive: true });
2688
2820
  } catch (err) {
2689
- if (process.env.ZIDANE_DEBUG) {
2690
- const cause = err instanceof Error ? err.message : String(err);
2691
- process.stderr.write(`[zidane/chat] project-db: mkdir "${projectDir}" failed: ${cause}\n`);
2692
- }
2821
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] project-db: mkdir "${projectDir}" failed: ${errorMessage(err)}\n`);
2693
2822
  return userOnlyPaths(userDir);
2694
2823
  }
2695
2824
  const effective = projectDir ?? userDir;
@@ -2803,6 +2932,98 @@ function useConfig() {
2803
2932
  return ctx;
2804
2933
  }
2805
2934
  //#endregion
2935
+ //#region src/chat/discovery-context.tsx
2936
+ const DiscoveryContext = createContext(null);
2937
+ function DiscoveryProvider({ value, children }) {
2938
+ return /* @__PURE__ */ jsx$1(DiscoveryContext.Provider, {
2939
+ value,
2940
+ children
2941
+ });
2942
+ }
2943
+ /**
2944
+ * Read live discovery state + actions. Throws if used outside a
2945
+ * `<DiscoveryProvider>` — discovery is a load-bearing dependency for
2946
+ * settings + completion popovers; bailing loud here surfaces wiring
2947
+ * mistakes at mount instead of producing empty catalogs at first
2948
+ * keystroke.
2949
+ */
2950
+ function useDiscovery() {
2951
+ const ctx = useContext(DiscoveryContext);
2952
+ if (!ctx) throw new Error("useDiscovery must be used inside <DiscoveryProvider>");
2953
+ return ctx;
2954
+ }
2955
+ /**
2956
+ * Non-throwing variant — returns `null` when no provider is mounted.
2957
+ * Used by composable modals (`<SettingsModal>`, …) that accept catalog
2958
+ * props as a fallback for embedders who don't wire the full TUI
2959
+ * shell. The in-app flow always mounts `<DiscoveryProvider>` so the
2960
+ * modal sees live state; standalone embeds get the prop snapshot.
2961
+ */
2962
+ function useDiscoveryOptional() {
2963
+ return useContext(DiscoveryContext);
2964
+ }
2965
+ //#endregion
2966
+ //#region src/chat/discovery-slot.ts
2967
+ function createDiscoverySlot(options) {
2968
+ const throttleMs = options.throttleMs ?? 3e3;
2969
+ const abortCtrl = new AbortController();
2970
+ let firstLoad = null;
2971
+ let refreshing = null;
2972
+ let lastScannedAt = 0;
2973
+ let aborted = false;
2974
+ const handleError = (err, phase) => {
2975
+ options.onError?.(err, phase);
2976
+ };
2977
+ const startRefresh = () => {
2978
+ lastScannedAt = Date.now();
2979
+ const run = options.walk(abortCtrl.signal).then((items) => {
2980
+ if (aborted) return;
2981
+ options.onLoad(items);
2982
+ }).catch((err) => {
2983
+ if (aborted) return;
2984
+ handleError(err, "refresh");
2985
+ }).finally(() => {
2986
+ if (refreshing === run) refreshing = null;
2987
+ });
2988
+ refreshing = run;
2989
+ return run;
2990
+ };
2991
+ const slot = {
2992
+ ensure() {
2993
+ if (aborted) return Promise.resolve([]);
2994
+ if (!firstLoad) {
2995
+ lastScannedAt = Date.now();
2996
+ firstLoad = options.walk(abortCtrl.signal).then((items) => {
2997
+ if (aborted) return [];
2998
+ options.onLoad(items);
2999
+ return items;
3000
+ }).catch((err) => {
3001
+ if (!aborted) handleError(err, "first-load");
3002
+ return [];
3003
+ });
3004
+ return firstLoad;
3005
+ }
3006
+ if (!refreshing && Date.now() - lastScannedAt >= throttleMs) startRefresh();
3007
+ return firstLoad;
3008
+ },
3009
+ refresh() {
3010
+ if (aborted) return Promise.resolve();
3011
+ if (refreshing) return refreshing;
3012
+ if (!firstLoad) return slot.ensure().then(() => {});
3013
+ return startRefresh();
3014
+ },
3015
+ isRefreshing() {
3016
+ return refreshing !== null;
3017
+ },
3018
+ abort() {
3019
+ if (aborted) return;
3020
+ aborted = true;
3021
+ abortCtrl.abort();
3022
+ }
3023
+ };
3024
+ return slot;
3025
+ }
3026
+ //#endregion
2806
3027
  //#region src/chat/themes/catppuccin.ts
2807
3028
  const LATTE = {
2808
3029
  rosewater: "#dc8a78",
@@ -3963,17 +4184,11 @@ async function listProjectFiles(opts = {}) {
3963
4184
  try {
3964
4185
  return toEntries(await listViaGit(cwd, signal), "git", maxFiles);
3965
4186
  } catch (err) {
3966
- if (process.env.ZIDANE_DEBUG) {
3967
- const cause = err instanceof Error ? err.message : String(err);
3968
- process.stderr.write(`[zidane/chat] git ls-files failed (${cause}) — falling back to fs walk\n`);
3969
- }
4187
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] git ls-files failed (${errorMessage(err)}) — falling back to fs walk\n`);
3970
4188
  try {
3971
4189
  return toEntries(await listViaFs(cwd, maxFiles, signal), "fs", maxFiles);
3972
4190
  } catch (fsErr) {
3973
- if (process.env.ZIDANE_DEBUG) {
3974
- const cause = fsErr instanceof Error ? fsErr.message : String(fsErr);
3975
- process.stderr.write(`[zidane/chat] fs walk failed: ${cause}\n`);
3976
- }
4191
+ if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] fs walk failed: ${errorMessage(fsErr)}\n`);
3977
4192
  return [];
3978
4193
  }
3979
4194
  }
@@ -4250,6 +4465,65 @@ function clip(text, max) {
4250
4465
  return text.length > max ? `${text.slice(0, max)}…` : text;
4251
4466
  }
4252
4467
  //#endregion
4468
+ //#region src/chat/hints.ts
4469
+ /**
4470
+ * Truncate `text` to at most `max` characters, replacing the trailing
4471
+ * overflow with `…`. Edge cases:
4472
+ * - `max <= 0` → empty string (no room to render at all).
4473
+ * - `max === 1` → just the ellipsis glyph.
4474
+ * - `text.length <= max` → unchanged.
4475
+ *
4476
+ * Trailing-style truncation matches the natural read order of titles:
4477
+ * the prefix carries enough signal to identify the surface.
4478
+ */
4479
+ function truncateTrailing(text, max) {
4480
+ if (max <= 0) return "";
4481
+ if (text.length <= max) return text;
4482
+ if (max === 1) return "…";
4483
+ return `${text.slice(0, max - 1)}…`;
4484
+ }
4485
+ /**
4486
+ * Plain-text width estimate for a list of {@link Hint}s rendered as
4487
+ * `<key> <label> · <key> <label> · …`. Exported so prompt-box overlays
4488
+ * can run the same responsive math as the bottom-bar footer when
4489
+ * deciding whether trigger hints fit. Pure / total.
4490
+ */
4491
+ function hintsLength(hints) {
4492
+ if (hints.length === 0) return 0;
4493
+ return hints.reduce((sum, h, i) => sum + hintLength(h) + (i > 0 ? 3 : 0), 0);
4494
+ }
4495
+ /** Plain-text width of a single hint as rendered by `renderHintSpans`. */
4496
+ function hintLength(h) {
4497
+ return h.key.length + 1 + h.label.length + (h.extra ? h.extra.key.length + 1 + h.extra.label.length : 0);
4498
+ }
4499
+ /** Stable empty list so callers can compare by reference. */
4500
+ const EMPTY_HINTS = Object.freeze([]);
4501
+ /**
4502
+ * Return the longest prefix of `hints` whose rendered width fits within
4503
+ * `budget`. Used to degrade hint rows gracefully at narrow terminal
4504
+ * widths instead of letting an absolutely-positioned hint row wrap
4505
+ * mid-segment (which paints the overflow over surrounding borders and
4506
+ * looks like garbled glyphs in a TUI).
4507
+ *
4508
+ * Prefix-only (no reordering, no last-hint priority) so the survivors
4509
+ * keep their authored order — the user's muscle memory for "leftmost
4510
+ * hint = primary action" stays intact as the terminal shrinks.
4511
+ */
4512
+ function clipHintsToWidth(hints, budget) {
4513
+ if (budget <= 0 || hints.length === 0) return EMPTY_HINTS;
4514
+ const out = [];
4515
+ let used = 0;
4516
+ for (let i = 0; i < hints.length; i++) {
4517
+ const h = hints[i];
4518
+ const cost = (i > 0 ? 3 : 0) + hintLength(h);
4519
+ if (used + cost > budget) break;
4520
+ out.push(h);
4521
+ used += cost;
4522
+ }
4523
+ if (out.length === 0) return EMPTY_HINTS;
4524
+ return out.length === hints.length ? hints : out;
4525
+ }
4526
+ //#endregion
4253
4527
  //#region src/chat/interactions.tsx
4254
4528
  const PRESENT_PLAN_TOOL = "present_plan";
4255
4529
  const ASK_USER_TOOL = "ask_user";
@@ -4717,6 +4991,79 @@ function makeRequestInteraction(actions) {
4717
4991
  });
4718
4992
  }
4719
4993
  //#endregion
4994
+ //#region src/chat/markdown-segments.ts
4995
+ /**
4996
+ * Split markdown text into alternating prose / fenced-code segments.
4997
+ *
4998
+ * Recognizes both ` ``` ` and `~~~` fences, with arbitrary length ≥ 3,
4999
+ * matching CommonMark's "fence indicator must be at least as long to
5000
+ * close" rule. Info-strings (the bit after the opening fence) are kept
5001
+ * as the `lang` hint; any trailing whitespace is trimmed. Closing
5002
+ * fences are detected on a line of their own (whitespace tolerated).
5003
+ *
5004
+ * Unclosed fences fall back to emitting the would-be-code body as a
5005
+ * trailing prose segment so no content is dropped — finalized markdown
5006
+ * isn't expected to ship one, but the fallback keeps the renderer
5007
+ * truthful when the model produces malformed output.
5008
+ *
5009
+ * Exported for unit tests.
5010
+ */
5011
+ function splitMarkdownCodeBlocks(text) {
5012
+ const lines = text.split("\n");
5013
+ const segments = [];
5014
+ let prose = [];
5015
+ let code = [];
5016
+ let inFence = false;
5017
+ let fenceChar = "";
5018
+ let fenceLen = 0;
5019
+ let lang = "";
5020
+ const flushProse = () => {
5021
+ if (prose.length > 0 && prose[prose.length - 1] === "") prose.pop();
5022
+ if (prose.length > 0) {
5023
+ segments.push({
5024
+ kind: "prose",
5025
+ content: prose.join("\n")
5026
+ });
5027
+ prose = [];
5028
+ }
5029
+ };
5030
+ let trimLeadingProseBlank = false;
5031
+ for (const line of lines) if (!inFence) {
5032
+ const open = line.match(/^(`{3,}|~{3,})([^\n`~]*)$/);
5033
+ if (open) {
5034
+ flushProse();
5035
+ inFence = true;
5036
+ fenceChar = open[1][0];
5037
+ fenceLen = open[1].length;
5038
+ lang = open[2].trim();
5039
+ code = [];
5040
+ continue;
5041
+ }
5042
+ if (trimLeadingProseBlank) {
5043
+ trimLeadingProseBlank = false;
5044
+ if (line === "" && prose.length === 0) continue;
5045
+ }
5046
+ prose.push(line);
5047
+ } else {
5048
+ const close = line.match(/^(`{3,}|~{3,})\s*$/);
5049
+ if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
5050
+ segments.push({
5051
+ kind: "code",
5052
+ content: code.join("\n"),
5053
+ lang
5054
+ });
5055
+ inFence = false;
5056
+ code = [];
5057
+ trimLeadingProseBlank = true;
5058
+ continue;
5059
+ }
5060
+ code.push(line);
5061
+ }
5062
+ if (inFence) prose.push(`${fenceChar.repeat(fenceLen)}${lang}`, ...code);
5063
+ flushProse();
5064
+ return segments;
5065
+ }
5066
+ //#endregion
4720
5067
  //#region src/chat/mcp-auth-state.ts
4721
5068
  /**
4722
5069
  * Apply one event to the state map. Pure, immutable — returns a new map
@@ -4812,11 +5159,10 @@ function readAll(dataDir) {
4812
5159
  }
4813
5160
  }
4814
5161
  function writeAll(dataDir, all) {
4815
- const path = mcpCredentialsPath(dataDir);
4816
- mkdirSync(dirname(path), { recursive: true });
4817
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
4818
- writeFileSync(tmp, `${JSON.stringify(all, null, 2)}\n`, { mode: FILE_MODE });
4819
- renameSync(tmp, path);
5162
+ writeFileAtomic(mcpCredentialsPath(dataDir), `${JSON.stringify(all, null, 2)}\n`, {
5163
+ ensureDir: true,
5164
+ mode: FILE_MODE
5165
+ });
4820
5166
  }
4821
5167
  /**
4822
5168
  * Build a file-backed `McpCredentialStore`. All per-server reads / writes
@@ -5069,7 +5415,7 @@ function discoverProjectMcps(opts = {}) {
5069
5415
  try {
5070
5416
  configs = parseMcpsFile(readFileSync(path, "utf8"));
5071
5417
  } catch (err) {
5072
- const message = err instanceof Error ? err.message : String(err);
5418
+ const message = errorMessage(err);
5073
5419
  errors.push({
5074
5420
  path,
5075
5421
  source,
@@ -5303,17 +5649,8 @@ function readProjects(dataDir) {
5303
5649
  } catch {}
5304
5650
  return {};
5305
5651
  }
5306
- function ensureDir(path) {
5307
- const dir = dirname(path);
5308
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
5309
- }
5310
- /** Atomic write — tmp + rename so a crash never leaves a half-file. */
5311
5652
  function writeProjects(dataDir, file) {
5312
- const path = projectsFilePath(dataDir);
5313
- ensureDir(path);
5314
- const tmp = `${path}.${process.pid}.tmp`;
5315
- writeFileSync(tmp, JSON.stringify(file, null, 2));
5316
- renameSync(tmp, path);
5653
+ writeFileAtomic(projectsFilePath(dataDir), JSON.stringify(file, null, 2), { ensureDir: true });
5317
5654
  }
5318
5655
  /**
5319
5656
  * Append `entry` to the safelist for `projectDir`, dedup-aware. Returns the
@@ -6040,6 +6377,19 @@ function useStreamBuffer(setEvents, options) {
6040
6377
  const tickerRef = useRef(null);
6041
6378
  const getSmoothRef = useRef(options?.getSmooth);
6042
6379
  getSmoothRef.current = options?.getSmooth;
6380
+ /**
6381
+ * Updaters queued while smooth-streaming was still draining backlog —
6382
+ * tool-call appends, finalize-markdown transforms, error events.
6383
+ * Applied in FIFO order at the END of `tick()` once every bucket is
6384
+ * empty, so the trailing typewriter characters land BEFORE the
6385
+ * follow-up event instead of being clobbered by an immediate drain.
6386
+ *
6387
+ * The buffer treats markdown deltas as the authoritative ordering
6388
+ * source: a tool call queued behind 100 buffered chars surfaces after
6389
+ * those 100 chars have typed out, preserving the visual stream order
6390
+ * the user is reading.
6391
+ */
6392
+ const pendingUpdatersRef = useRef([]);
6043
6393
  const stopTicker = useCallback(() => {
6044
6394
  if (tickerRef.current) {
6045
6395
  clearInterval(tickerRef.current);
@@ -6047,27 +6397,45 @@ function useStreamBuffer(setEvents, options) {
6047
6397
  }
6048
6398
  }, []);
6049
6399
  /**
6050
- * Drain every bucket in full and stop the ticker. Used for turn-boundary
6051
- * commits (`flush` / `appendImmediate` / `flushAndUpdate`) and teardown
6052
- * (`reset`). The accompanying `updater` (when provided) runs against the
6053
- * post-drain event list so consumers can append a synchronous event or
6054
- * apply a transform in the same `setEvents` call.
6400
+ * Has at least one bucket got unflushed *markdown* content?
6401
+ *
6402
+ * Thinking content is excluded: it always flushes in full on every
6403
+ * tick (see `tick()`), so its presence in a bucket only matters until
6404
+ * the next 16 ms cycle never long enough to justify deferring an
6405
+ * end-of-turn updater.
6055
6406
  */
6056
- const drainAll = useCallback((updater) => {
6407
+ const hasMarkdownBacklog = useCallback(() => {
6408
+ for (const bucket of bucketsRef.current.values()) if (bucket.markdown.length > 0) return true;
6409
+ return false;
6410
+ }, []);
6411
+ /**
6412
+ * Drain every bucket in full and synchronously run any queued
6413
+ * post-drain updaters plus the caller-provided one. Used by the
6414
+ * fast paths below — `reset`, batched-mode `appendImmediate` /
6415
+ * `flushAndUpdate`, and smooth-mode calls where no markdown
6416
+ * backlog remains.
6417
+ */
6418
+ const drainNow = useCallback((updater) => {
6057
6419
  stopTicker();
6058
6420
  const buckets = Array.from(bucketsRef.current.values());
6059
6421
  bucketsRef.current.clear();
6060
- if (!buckets.some((b) => b.markdown.length > 0 || b.thinking.length > 0) && !updater) return;
6422
+ const queued = pendingUpdatersRef.current;
6423
+ pendingUpdatersRef.current = [];
6424
+ if (!buckets.some((b) => b.markdown.length > 0 || b.thinking.length > 0) && !updater && queued.length === 0) return;
6061
6425
  setEvents((prev) => {
6062
6426
  let merged = prev;
6063
6427
  for (const bucket of buckets) merged = applyBucket(merged, bucket);
6064
- return updater ? updater(merged) : merged;
6428
+ for (const u of queued) merged = u(merged);
6429
+ if (updater) merged = updater(merged);
6430
+ return merged;
6065
6431
  });
6066
6432
  }, [setEvents, stopTicker]);
6067
6433
  /**
6068
6434
  * One tick of the continuous drain loop. Walks every live bucket and
6069
- * commits a portion of its content based on the current mode. Stops the
6070
- * ticker once every bucket is empty.
6435
+ * commits a portion of its content based on the current mode. Once
6436
+ * every bucket empties out, fires any post-drain updaters that were
6437
+ * queued while smooth streaming was still in progress (turn
6438
+ * finalize, tool calls, errors) and stops the ticker.
6071
6439
  *
6072
6440
  * `thinking` content always flushes immediately even in smooth mode —
6073
6441
  * it's an internal-reasoning surface, surfaced for transparency rather
@@ -6100,9 +6468,11 @@ function useStreamBuffer(setEvents, options) {
6100
6468
  });
6101
6469
  if (bucket.markdown.length > 0) stillHasContent = true;
6102
6470
  }
6103
- if (portions.length > 0) setEvents((prev) => {
6471
+ const queued = !stillHasContent && pendingUpdatersRef.current.length > 0 ? pendingUpdatersRef.current.splice(0, pendingUpdatersRef.current.length) : [];
6472
+ if (portions.length > 0 || queued.length > 0) setEvents((prev) => {
6104
6473
  let next = prev;
6105
6474
  for (const portion of portions) next = applyBucket(next, portion);
6475
+ for (const u of queued) next = u(next);
6106
6476
  return next;
6107
6477
  });
6108
6478
  if (!stillHasContent) stopTicker();
@@ -6111,9 +6481,40 @@ function useStreamBuffer(setEvents, options) {
6111
6481
  if (tickerRef.current) return;
6112
6482
  tickerRef.current = setInterval(tick, TICK_INTERVAL_MS);
6113
6483
  }, [tick]);
6114
- const flush = useCallback(() => drainAll(), [drainAll]);
6115
- const flushAndUpdate = useCallback((update) => drainAll(update), [drainAll]);
6116
- const appendImmediate = useCallback((evt) => drainAll((events) => [...events, evt]), [drainAll]);
6484
+ /**
6485
+ * Decide between fast-drain and deferred-drain for an
6486
+ * updater-bearing call (`appendImmediate`, `flushAndUpdate`).
6487
+ *
6488
+ * Smooth mode + markdown backlog → ENQUEUE the updater behind the
6489
+ * trailing characters; the ticker picks it up once buckets empty.
6490
+ * Anything else → fall through to `drainNow` so the call retains
6491
+ * its synchronous semantics.
6492
+ */
6493
+ const drainOrDefer = useCallback((updater) => {
6494
+ if ((getSmoothRef.current?.() ?? true) && hasMarkdownBacklog()) {
6495
+ pendingUpdatersRef.current.push(updater);
6496
+ ensureTicker();
6497
+ return;
6498
+ }
6499
+ drainNow(updater);
6500
+ }, [
6501
+ drainNow,
6502
+ ensureTicker,
6503
+ hasMarkdownBacklog
6504
+ ]);
6505
+ const flush = useCallback(() => {
6506
+ if ((getSmoothRef.current?.() ?? true) && hasMarkdownBacklog()) {
6507
+ ensureTicker();
6508
+ return;
6509
+ }
6510
+ drainNow();
6511
+ }, [
6512
+ drainNow,
6513
+ ensureTicker,
6514
+ hasMarkdownBacklog
6515
+ ]);
6516
+ const flushAndUpdate = useCallback((update) => drainOrDefer(update), [drainOrDefer]);
6517
+ const appendImmediate = useCallback((evt) => drainOrDefer((events) => [...events, evt]), [drainOrDefer]);
6117
6518
  const queueStreamDelta = useCallback((kind, delta, source) => {
6118
6519
  if (!delta) return;
6119
6520
  const owner = source?.childId ?? PARENT_OWNER;
@@ -6130,6 +6531,7 @@ function useStreamBuffer(setEvents, options) {
6130
6531
  const reset = useCallback(() => {
6131
6532
  stopTicker();
6132
6533
  bucketsRef.current.clear();
6534
+ pendingUpdatersRef.current = [];
6133
6535
  }, [stopTicker]);
6134
6536
  return useMemo(() => ({
6135
6537
  queueStreamDelta,
@@ -6422,6 +6824,46 @@ function formatBytes(bytes) {
6422
6824
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6423
6825
  }
6424
6826
  //#endregion
6827
+ //#region src/chat/transcript-anchors.ts
6828
+ /**
6829
+ * Per-item anchor ids for auto-scroll. Walks `items` in render order
6830
+ * and, for each event, returns either:
6831
+ * - `'turn-anchor-<turnId>'` — the first event of this turn (the
6832
+ * scroll target).
6833
+ * - `undefined` — later event of an already-tagged turn (or a
6834
+ * synthetic event with no `turnId`).
6835
+ *
6836
+ * `ids[i]` is a tuple per item: length 1 for plain events, length N
6837
+ * for subagent runs (one entry per inner event). `idByTurn` is the
6838
+ * inverse lookup used by the scroll effect. `lastTurnId` is the
6839
+ * most-recently-rendered turn — the scroll effect special-cases it to
6840
+ * snap to bottom rather than scroll the anchor into view (which would
6841
+ * stop short of the actual tail).
6842
+ *
6843
+ * Exported so the anchor-tagging matrix can be unit-tested without
6844
+ * rendering anything.
6845
+ */
6846
+ function computeTurnAnchors(items) {
6847
+ const idByTurn = /* @__PURE__ */ new Map();
6848
+ let lastTurnId;
6849
+ const tag = (turnId) => {
6850
+ if (!turnId) return void 0;
6851
+ lastTurnId = turnId;
6852
+ if (idByTurn.has(turnId)) return void 0;
6853
+ const id = `turn-anchor-${turnId}`;
6854
+ idByTurn.set(turnId, id);
6855
+ return id;
6856
+ };
6857
+ const ids = [];
6858
+ for (const item of items) if (item.kind === "event") ids.push([tag(item.event.turnId)]);
6859
+ else ids.push(item.events.map((e) => tag(e.turnId)));
6860
+ return {
6861
+ idByTurn,
6862
+ ids,
6863
+ lastTurnId
6864
+ };
6865
+ }
6866
+ //#endregion
6425
6867
  //#region src/chat/turn-operations.ts
6426
6868
  /**
6427
6869
  * Fork — keep every turn up to and including `turnId`, then strip any
@@ -6537,6 +6979,6 @@ function countNeighbors(turnIds, turnId) {
6537
6979
  };
6538
6980
  }
6539
6981
  //#endregion
6540
- export { getMcpAuthStatus as $, openrouterDescriptor as $n, toolCallPreview as $t, isOnSafelist as A, mergeReferences as An, VAPORWAVE_THEME as At, filterModelCatalog as B, setProviderCredential as Bn, eventsFromTurns as Bt, writeSessionExport as C, uniqueSkillNamesFromReferences as Cn, SettingsProvider as Ct, IMPLICITLY_SAFE_TOOLS as D, applyInsert as Dn, DEFAULT_THEME as Dt, useSafeModeQueue as E, uniqueFilesFromReferences as En, BUILTIN_THEMES as Et, writeProjects as F, applyApiKeyEnv as Fn, ConfigProvider as Ft, parseMcpsFile as G, cerebrasDescriptor as Gn, listSessionMeta as Gt, buildMcpServers as H, BUILTIN_PROVIDERS as Hn, isTurnHighlighted as Ht, splitPromptSegments as I, credentialsPath as In, useConfig as It, mcpCredentialsPath as J, getContextWindow as Jn, saveState as Jt, projectUserPaths as K, credKeyOf as Kn, loadState as Kt, runOAuthLogin as L, readCredentials as Ln, resolveConfig as Lt, projectsFilePath as M, tryOpenBrowser as Mn, CATPPUCCIN_LATTE as Mt, readProjects as N, shouldAutoCompact as Nn, CATPPUCCIN_MACCHIATO as Nt, addToSafelist as O, collectReferences as On, resolveChipColor as Ot, suggestSafelistEntry as P, detectAuth as Pn, CATPPUCCIN_MOCHA as Pt, useMcpAuthState as Q, openaiDescriptor as Qn, titleFromTurns as Qt, supportsOAuth as R, readProviderCredential as Rn, createStateStore as Rt, resolveSessionExportTarget as S, createSkillsCompletionProvider as Sn, SETTINGS_TOGGLES as St, useSafeModeActions as T, createFilesCompletionProvider as Tn, useSettings as Tt, defaultMcpsConfigPaths as U, OUTPUT_RESERVE_TOKENS as Un, isVisible as Ut, indexOfEntry as V, writeCredentials as Vn, isEditErrorResult as Vt, discoverProjectMcps as W, anthropicDescriptor as Wn, lastContextSizeFromTurns as Wt, McpAuthProvider as X, modelSupportsReasoning as Xn, stripSpawnTokensLine as Xt, patchMcpCredential as Y, getModelInfo as Yn, selectableTurnIds as Yt, useMcpAuthDispatch as Z, modelsForDescriptor as Zn, sumRunCosts as Zt, useStreamBuffer as _, mergeKeybindings as _n, SUBAGENT_GUIDANCE as _r, shortId as _t, TOOL_DISPLAY as a, computeLineDiff as an, PLAN_AGENT as ar, createInteractionTools as at, discoverProjectSkills as b, stripJsonComments as bn, buildPlanSystem as br, DEFAULT_SETTINGS as bt, ThemeProvider as c, splitLines as cn, singleAgentRegistry as cr, pendingInteractionsFromTurns as ct, useSurfaces as d, DEFAULT_KEYBINDINGS as dn, DOING_TASKS_DOCTRINE as dr, useInteractionsQueue as dt, toolResultText as en, piIdOf as er, reduceMcpAuth as et, useSyntaxStyles as f, KEYBINDING_DEFS as fn, IDENTITY_PREFIX as fr, cleanTitle as ft, turnContextSize as g, matchesBinding as gn, PLAN_MODE_DOCTRINE_NO_PROMPTS as gr, fmtTokens as gt, finalizeStreamingMarkdownForOwner as h, keybindingsPath as hn, PLAN_MODE_DOCTRINE as hr, compactPath as ht, turnAsText as i, computeInlineDiff as in, DEFAULT_PERSIST_EXCLUDE_TOOLS as ir, buildResumedToolResultsTurn as it, matchesSafelistEntry as j, useCompletion as jn, CATPPUCCIN_FRAPPE as jt, getSafelist as k, findActiveTrigger as kn, resolveTheme as kt, useColors as l, tokenize as ln, ACTIONS_WITH_CARE_DOCTRINE as lr, serializeInteractionResponse as lt, finalizeStreamingMarkdown as m, ensureKeybindingsFile as mn, INTERACTION_GUIDANCE_NO_PROMPTS as mr, ageString as mt, deleteTurnSafely as n, buildContextualDiff as nn, BUILTIN_AGENTS as nr, InteractionsProvider as nt, displayNameFor as o, extractEditPayload as on, accentColor as or, isInteractionTool as ot, useTheme as p, KEYBINDING_DEF_BY_ACTION as pn, INTERACTION_GUIDANCE as pr, generateSessionTitle as pt, createFileMcpCredentialStore as q, effectiveContextWindow as qn, marginTopFor as qt, truncateTurnsAt as r, buildUnifiedDiff as rn, DEFAULT_AGENT_ID as rr, PRESENT_PLAN_TOOL as rt, formatToolCall as s, filetypeFromPath as sn, resolveAgentId as sr, makeRequestInteraction as st, countNeighbors as t, turnSelectionOwnership as tn, BUILD_AGENT as tr, ASK_USER_TOOL as tt, useSelectStyle as u, findGitRoot$1 as un, COMMUNICATION_DOCTRINE as ur, useInteractionsActions as ut, buildSkillsConfig as v, parseBindingSpec as vn, TOKEN_DISCIPLINE_DOCTRINE as vr, listProjectFiles as vt, SafeModeProvider as w, FILES_TRIGGER as wn, clampFps as wt, renderSession as x, SKILLS_TRIGGER as xn, envSection as xr, SETTINGS_CHOICES as xt, defaultSkillScanPaths as y, readKeybindings as yn, buildBuildSystem as yr, useEnabledToggleSet as yt, buildModelCatalog as z, removeProviderCredential as zn, deriveSessionTitle as zt };
6982
+ export { useMcpAuthState as $, setProviderCredential as $n, lastContextSizeFromTurns as $t, getSafelist as A, SKILLS_TRIGGER as An, SUBAGENT_GUIDANCE as Ar, useSettings as At, buildModelCatalog as B, useCompletion as Bn, createDiscoverySlot as Bt, resolveSessionExportTarget as C, ensureKeybindingsFile as Cn, COMMUNICATION_DOCTRINE as Cr, listProjectFiles as Ct, useSafeModeQueue as D, parseBindingSpec as Dn, INTERACTION_GUIDANCE_NO_PROMPTS as Dr, SETTINGS_TOGGLES as Dt, useSafeModeActions as E, mergeKeybindings as En, INTERACTION_GUIDANCE as Er, SETTINGS_CHOICES as Et, suggestSafelistEntry as F, uniqueFilesFromReferences as Fn, VAPORWAVE_THEME as Ft, discoverProjectMcps as G, bootTick as Gn, useConfig as Gt, indexOfEntry as H, buildLinearRamp as Hn, useDiscovery as Ht, writeProjects as I, applyInsert as In, CATPPUCCIN_FRAPPE as It, createFileMcpCredentialStore as J, applyApiKeyEnv as Jn, deriveSessionTitle as Jt, parseMcpsFile as K, shouldAutoCompact as Kn, resolveConfig as Kt, splitPromptSegments as L, collectReferences as Ln, CATPPUCCIN_LATTE as Lt, matchesSafelistEntry as M, uniqueSkillNamesFromReferences as Mn, buildBuildSystem as Mr, DEFAULT_THEME as Mt, projectsFilePath as N, FILES_TRIGGER as Nn, buildPlanSystem as Nr, resolveChipColor as Nt, IMPLICITLY_SAFE_TOOLS as O, readKeybindings as On, PLAN_MODE_DOCTRINE as Or, SettingsProvider as Ot, readProjects as P, createFilesCompletionProvider as Pn, envSection as Pr, resolveTheme as Pt, useMcpAuthDispatch as Q, removeProviderCredential as Qn, isVisible as Qt, runOAuthLogin as R, findActiveTrigger as Rn, CATPPUCCIN_MACCHIATO as Rt, renderSession as S, KEYBINDING_DEF_BY_ACTION as Sn, ACTIONS_WITH_CARE_DOCTRINE as Sr, shortId as St, SafeModeProvider as T, matchesBinding as Tn, IDENTITY_PREFIX as Tr, DEFAULT_SETTINGS as Tt, buildMcpServers as U, tryOpenBrowser as Un, useDiscoveryOptional as Ut, filterModelCatalog as V, blendHsl as Vn, DiscoveryProvider as Vt, defaultMcpsConfigPaths as W, bootProfileEnabled as Wn, ConfigProvider as Wt, patchMcpCredential as X, readCredentials as Xn, isEditErrorResult as Xt, mcpCredentialsPath as Y, credentialsPath as Yn, eventsFromTurns as Yt, McpAuthProvider as Z, readProviderCredential as Zn, isTurnHighlighted as Zt, turnContextSize as _, splitLines as _n, DEFAULT_PERSIST_EXCLUDE_TOOLS as _r, cleanTitle as _t, computeTurnAnchors as a, stripSpawnTokensLine as an, credKeyOf as ar, PRESENT_PLAN_TOOL as at, defaultSkillScanPaths as b, DEFAULT_KEYBINDINGS as bn, resolveAgentId as br, compactPath as bt, formatToolCall as c, toolCallPreview as cn, getModelInfo as cr, isInteractionTool as ct, useSelectStyle as d, buildContextualDiff as dn, openaiDescriptor as dr, serializeInteractionResponse as dt, listSessionMeta as en, writeCredentials as er, getMcpAuthStatus as et, useSurfaces as f, buildUnifiedDiff as fn, openrouterDescriptor as fr, useInteractionsActions as ft, finalizeStreamingMarkdownForOwner as g, filetypeFromPath as gn, DEFAULT_AGENT_ID as gr, truncateTrailing as gt, finalizeStreamingMarkdown as h, extractEditPayload as hn, BUILTIN_AGENTS as hr, hintsLength as ht, turnAsText as i, selectableTurnIds as in, cerebrasDescriptor as ir, InteractionsProvider as it, isOnSafelist as j, createSkillsCompletionProvider as jn, TOKEN_DISCIPLINE_DOCTRINE as jr, BUILTIN_THEMES as jt, addToSafelist as k, stripJsonComments as kn, PLAN_MODE_DOCTRINE_NO_PROMPTS as kr, clampFps as kt, ThemeProvider as l, toolResultText as ln, modelSupportsReasoning as lr, makeRequestInteraction as lt, useTheme as m, computeLineDiff as mn, BUILD_AGENT as mr, clipHintsToWidth as mt, deleteTurnSafely as n, marginTopFor as nn, OUTPUT_RESERVE_TOKENS as nr, splitMarkdownCodeBlocks as nt, TOOL_DISPLAY as o, sumRunCosts as on, effectiveContextWindow as or, buildResumedToolResultsTurn as ot, useSyntaxStyles as p, computeInlineDiff as pn, piIdOf as pr, useInteractionsQueue as pt, projectUserPaths as q, detectAuth as qn, createStateStore as qt, truncateTurnsAt as r, saveState as rn, anthropicDescriptor as rr, ASK_USER_TOOL as rt, displayNameFor as s, titleFromTurns as sn, getContextWindow as sr, createInteractionTools as st, countNeighbors as t, loadState as tn, BUILTIN_PROVIDERS as tr, reduceMcpAuth as tt, useColors as u, turnSelectionOwnership as un, modelsForDescriptor as ur, pendingInteractionsFromTurns as ut, useStreamBuffer as v, tokenize as vn, PLAN_AGENT as vr, generateSessionTitle as vt, writeSessionExport as w, keybindingsPath as wn, DOING_TASKS_DOCTRINE as wr, useEnabledToggleSet as wt, discoverProjectSkills as x, KEYBINDING_DEFS as xn, singleAgentRegistry as xr, fmtTokens as xt, buildSkillsConfig as y, findGitRoot$1 as yn, accentColor as yr, ageString as yt, supportsOAuth as z, mergeReferences as zn, CATPPUCCIN_MOCHA as zt };
6541
6983
 
6542
- //# sourceMappingURL=turn-operations-CDmQ2h-T.js.map
6984
+ //# sourceMappingURL=turn-operations-DNKpDGQi.js.map