dsh-code 1.0.1 → 1.0.3

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 (44) hide show
  1. package/README.en.md +21 -13
  2. package/README.md +21 -13
  3. package/lib/index.mjs +1902 -1092
  4. package/lib/types/app.d.ts +4 -13
  5. package/lib/types/attachments.d.ts +1 -1
  6. package/lib/types/editor-keys.d.ts +105 -0
  7. package/lib/types/git-workflow.d.ts +6 -2
  8. package/lib/types/keyboard.d.ts +31 -0
  9. package/lib/types/mentions.d.ts +2 -0
  10. package/lib/types/model-capabilities.d.ts +82 -0
  11. package/lib/types/provider-settings.d.ts +7 -0
  12. package/lib/types/render/animations.d.ts +27 -11
  13. package/lib/types/render/editor.d.ts +32 -7
  14. package/lib/types/render/lines.d.ts +26 -1
  15. package/lib/types/render/markdown.d.ts +1 -1
  16. package/lib/types/render/projection.d.ts +15 -1
  17. package/lib/types/render/text.d.ts +15 -9
  18. package/lib/types/render/width.d.ts +29 -0
  19. package/lib/types/session-directory.d.ts +27 -0
  20. package/lib/types/settings-file.d.ts +33 -0
  21. package/lib/types/store.d.ts +10 -0
  22. package/lib/types/subagents.d.ts +13 -3
  23. package/package.json +159 -159
  24. package/src/app.ts +920 -764
  25. package/src/attachments.ts +7 -0
  26. package/src/editor-keys.ts +371 -0
  27. package/src/git-workflow.ts +10 -6
  28. package/src/index.ts +1637 -1523
  29. package/src/internals.ts +26 -9
  30. package/src/keyboard.ts +131 -7
  31. package/src/mentions.ts +6 -1
  32. package/src/model-capabilities.ts +318 -0
  33. package/src/provider-settings.ts +16 -0
  34. package/src/render/animations.ts +64 -17
  35. package/src/render/editor.ts +125 -25
  36. package/src/render/lines.ts +403 -342
  37. package/src/render/markdown.ts +4 -7
  38. package/src/render/projection.ts +63 -40
  39. package/src/render/text.ts +152 -150
  40. package/src/render/width.ts +189 -0
  41. package/src/session-directory.ts +56 -0
  42. package/src/settings-file.ts +56 -0
  43. package/src/store.ts +26 -7
  44. package/src/subagents.ts +39 -6
package/lib/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { readFileSync, realpathSync } from "node:fs";
6
6
  import os, { homedir } from "node:os";
7
- import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
7
+ import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
@@ -14,12 +14,12 @@ import { PassThrough, Stream } from "node:stream";
14
14
  import process$1, { cwd, env } from "node:process";
15
15
  import { EventEmitter } from "node:events";
16
16
  import { Buffer as Buffer$1 } from "node:buffer";
17
+ import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
18
+ import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
17
19
  import { execFile, spawn } from "node:child_process";
18
20
  import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
19
21
  import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
20
22
  import { fileURLToPath } from "node:url";
21
- import { DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES, DEFAULT_FILE_SEARCH_MAX_ENTRIES, DEFAULT_FILE_SEARCH_MAX_RESULTS, WorkspaceFileSearch } from "@deepseek-ai/dsh-file-reference-local";
22
- import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
23
23
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
24
24
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
25
25
  //#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
@@ -21657,16 +21657,16 @@ var import_emoji_regex = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(
21657
21657
  return /[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26D3\uFE0F?(?:\u200D\uD83D\uDCA5)?|\u26F9(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF43\uDF45-\uDF4A\uDF4C-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDF44(?:\u200D\uD83D\uDFEB)?|\uDF4B(?:\u200D\uD83D\uDFE9)?|\uDFC3(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E-\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4\uDEB5](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE41\uDE43\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC08(?:\u200D\u2B1B)?|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC26(?:\u200D(?:\u2B1B|\uD83D\uDD25))?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDD1D\uDEEF]\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE]|[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE]|\uDEEF\u200D\uD83D\uDC69\uD83C[\uDFFB-\uDFFE])))?))?|\uDD75(?:\uD83C[\uDFFB-\uDFFF]|\uFE0F)?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?|\uDE42(?:\u200D[\u2194\u2195]\uFE0F?)?|\uDEB6(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3C-\uDD3E\uDDB8\uDDB9\uDDCD\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC2\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF]|\uDDCE(?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D(?:[\u2640\u2642]\uFE0F?(?:\u200D\u27A1\uFE0F?)?|\u27A1\uFE0F?))?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1|\uDDD1\u200D\uD83E\uDDD2(?:\u200D\uD83E\uDDD2)?|\uDDD2(?:\u200D\uD83E\uDDD2)?))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC30\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])|\uD83E(?:[\uDDAF\uDDBC\uDDBD](?:\u200D\u27A1\uFE0F?)?|[\uDDB0-\uDDB3\uDE70]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF]|\uDEEF\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)/g;
21658
21658
  };
21659
21659
  })))(), 1);
21660
- const segmenter$1 = new Intl.Segmenter();
21660
+ const segmenter$2 = new Intl.Segmenter();
21661
21661
  const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
21662
- function stringWidth(string, options = {}) {
21662
+ function stringWidth$1(string, options = {}) {
21663
21663
  if (typeof string !== "string" || string.length === 0) return 0;
21664
21664
  const { ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = options;
21665
21665
  if (!countAnsiEscapeCodes) string = stripAnsi(string);
21666
21666
  if (string.length === 0) return 0;
21667
21667
  let width = 0;
21668
21668
  const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
21669
- for (const { segment: character } of segmenter$1.segment(string)) {
21669
+ for (const { segment: character } of segmenter$2.segment(string)) {
21670
21670
  const codePoint = character.codePointAt(0);
21671
21671
  if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) continue;
21672
21672
  if (codePoint >= 8203 && codePoint <= 8207 || codePoint === 65279) continue;
@@ -21686,7 +21686,7 @@ function stringWidth(string, options = {}) {
21686
21686
  //#region node_modules/.pnpm/widest-line@5.0.0/node_modules/widest-line/index.js
21687
21687
  function widestLine(string) {
21688
21688
  let lineWidth = 0;
21689
- for (const line of string.split("\n")) lineWidth = Math.max(lineWidth, stringWidth(line));
21689
+ for (const line of string.split("\n")) lineWidth = Math.max(lineWidth, stringWidth$1(line));
21690
21690
  return lineWidth;
21691
21691
  }
21692
21692
  //#endregion
@@ -21885,14 +21885,14 @@ const ANSI_SGR_TERMINATOR = "m";
21885
21885
  const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
21886
21886
  const wrapAnsiCode = (code) => `${ESCAPES$3.values().next().value}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
21887
21887
  const wrapAnsiHyperlink = (url) => `${ESCAPES$3.values().next().value}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
21888
- const wordLengths = (string) => string.split(" ").map((character) => stringWidth(character));
21888
+ const wordLengths = (string) => string.split(" ").map((character) => stringWidth$1(character));
21889
21889
  const wrapWord = (rows, word, columns) => {
21890
21890
  const characters = [...word];
21891
21891
  let isInsideEscape = false;
21892
21892
  let isInsideLinkEscape = false;
21893
- let visible = stringWidth(stripAnsi(rows.at(-1)));
21893
+ let visible = stringWidth$1(stripAnsi(rows.at(-1)));
21894
21894
  for (const [index, character] of characters.entries()) {
21895
- const characterLength = stringWidth(character);
21895
+ const characterLength = stringWidth$1(character);
21896
21896
  if (visible + characterLength <= columns) rows[rows.length - 1] += character;
21897
21897
  else {
21898
21898
  rows.push(character);
@@ -21923,7 +21923,7 @@ const stringVisibleTrimSpacesRight = (string) => {
21923
21923
  const words = string.split(" ");
21924
21924
  let last = words.length;
21925
21925
  while (last > 0) {
21926
- if (stringWidth(words[last - 1]) > 0) break;
21926
+ if (stringWidth$1(words[last - 1]) > 0) break;
21927
21927
  last--;
21928
21928
  }
21929
21929
  if (last === words.length) return string;
@@ -21938,7 +21938,7 @@ const exec = (string, columns, options = {}) => {
21938
21938
  let rows = [""];
21939
21939
  for (const [index, word] of string.split(" ").entries()) {
21940
21940
  if (options.trim !== false) rows[rows.length - 1] = rows.at(-1).trimStart();
21941
- let rowLength = stringWidth(rows.at(-1));
21941
+ let rowLength = stringWidth$1(rows.at(-1));
21942
21942
  if (index !== 0) {
21943
21943
  if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
21944
21944
  rows.push("");
@@ -22086,7 +22086,7 @@ function cliTruncate(text, columns, options = {}) {
22086
22086
  if (typeof columns !== "number") throw new TypeError(`Expected \`columns\` to be a number, got ${typeof columns}`);
22087
22087
  if (columns < 1) return "";
22088
22088
  if (columns === 1) return truncationCharacter;
22089
- const length = stringWidth(text);
22089
+ const length = stringWidth$1(text);
22090
22090
  if (length <= columns) return text;
22091
22091
  if (position === "start") {
22092
22092
  if (preferTruncationOnSpace) {
@@ -22094,7 +22094,7 @@ function cliTruncate(text, columns, options = {}) {
22094
22094
  return truncationCharacter + sliceAnsi$1(text, nearestSpace, length).trim();
22095
22095
  }
22096
22096
  if (space === true) truncationCharacter += " ";
22097
- return truncationCharacter + sliceAnsi$1(text, length - columns + stringWidth(truncationCharacter), length);
22097
+ return truncationCharacter + sliceAnsi$1(text, length - columns + stringWidth$1(truncationCharacter), length);
22098
22098
  }
22099
22099
  if (position === "middle") {
22100
22100
  if (space === true) truncationCharacter = ` ${truncationCharacter} `;
@@ -22104,12 +22104,12 @@ function cliTruncate(text, columns, options = {}) {
22104
22104
  const spaceNearSecondBreakPoint = getIndexOfNearestSpace(text, length - (columns - half) + 1, true);
22105
22105
  return sliceAnsi$1(text, 0, spaceNearFirstBreakPoint) + truncationCharacter + sliceAnsi$1(text, spaceNearSecondBreakPoint, length).trim();
22106
22106
  }
22107
- return sliceAnsi$1(text, 0, half) + truncationCharacter + sliceAnsi$1(text, length - (columns - half) + stringWidth(truncationCharacter), length);
22107
+ return sliceAnsi$1(text, 0, half) + truncationCharacter + sliceAnsi$1(text, length - (columns - half) + stringWidth$1(truncationCharacter), length);
22108
22108
  }
22109
22109
  if (position === "end") {
22110
22110
  if (preferTruncationOnSpace) return sliceAnsi$1(text, 0, getIndexOfNearestSpace(text, columns - 1)) + truncationCharacter;
22111
22111
  if (space === true) truncationCharacter = ` ${truncationCharacter}`;
22112
- return sliceAnsi$1(text, 0, columns - stringWidth(truncationCharacter)) + truncationCharacter;
22112
+ return sliceAnsi$1(text, 0, columns - stringWidth$1(truncationCharacter)) + truncationCharacter;
22113
22113
  }
22114
22114
  throw new Error(`Expected \`options.position\` to be either \`start\`, \`middle\` or \`end\`, got ${position}`);
22115
22115
  }
@@ -23064,7 +23064,7 @@ var Output = class {
23064
23064
  if (clipHorizontally) {
23065
23065
  lines = lines.map((line) => {
23066
23066
  const from = x < clip.x1 ? clip.x1 - x : 0;
23067
- const width = stringWidth(line);
23067
+ const width = stringWidth$1(line);
23068
23068
  return sliceAnsi(line, from, x + width > clip.x2 ? clip.x2 - x : width);
23069
23069
  });
23070
23070
  if (x < clip.x1) x = clip.x1;
@@ -24355,6 +24355,180 @@ function followInspectorCursor(cursor, previousLength, nextLength) {
24355
24355
  return Math.min(cursor, nextLast);
24356
24356
  }
24357
24357
  //#endregion
24358
+ //#region src/render/width.ts
24359
+ /**
24360
+ * Precise terminal-cell width measurement — the single authority every
24361
+ * budget, wrap, and truncation path shares. The previous heuristic
24362
+ * (`codePoint > 0x2e7f ? 2 : 1)) mis-sized Hangul Jamo (narrow), high
24363
+ * non-CJK code points (wide), and emoji: text-default glyphs like ✳ ⚠ ❤
24364
+ * counted 2 while terminals draw 1, and VS16 sequences counted 1 while
24365
+ * terminals draw 2 — the exact drift class the community dsh-TUI string
24366
+ * engine documents (a spinner glyph drifting one column per frame). This
24367
+ * module adapts that engine's rules without its Ink-fork renderer: an ASCII
24368
+ * fast path, grapheme-cluster iteration via Intl.Segmenter (code-point
24369
+ * fallback), a merged East-Asian-Wide/Fullwidth + Emoji_Presentation range
24370
+ * table, text-default emoji = 1, VS16 = 2, marks/selectors/ZWJ = 0.
24371
+ * @module @deepseek-ai/dsh-code/render/width
24372
+ */
24373
+ /**
24374
+ * Code-point ranges rendered two cells wide: East Asian Wide/Fullwidth
24375
+ * blocks merged with the Emoji_Presentation set (default-wide emoji) and
24376
+ * regional indicators (flags). Sorted, inclusive, binary-searched.
24377
+ */
24378
+ const WIDE_RANGES = [
24379
+ [4352, 4447],
24380
+ [8986, 8987],
24381
+ [9001, 9002],
24382
+ [9193, 9196],
24383
+ [9200, 9200],
24384
+ [9203, 9203],
24385
+ [9208, 9210],
24386
+ [9725, 9726],
24387
+ [9748, 9749],
24388
+ [9800, 9811],
24389
+ [9855, 9855],
24390
+ [9875, 9875],
24391
+ [9889, 9889],
24392
+ [9898, 9899],
24393
+ [9917, 9918],
24394
+ [9924, 9925],
24395
+ [9934, 9934],
24396
+ [9940, 9940],
24397
+ [9962, 9962],
24398
+ [9970, 9971],
24399
+ [9973, 9973],
24400
+ [9978, 9978],
24401
+ [9981, 9981],
24402
+ [9989, 9989],
24403
+ [9994, 9995],
24404
+ [10024, 10024],
24405
+ [10060, 10060],
24406
+ [10062, 10062],
24407
+ [10067, 10069],
24408
+ [10071, 10071],
24409
+ [10133, 10135],
24410
+ [10160, 10160],
24411
+ [10175, 10175],
24412
+ [11035, 11036],
24413
+ [11088, 11088],
24414
+ [11093, 11093],
24415
+ [11904, 12350],
24416
+ [12353, 13311],
24417
+ [13312, 19903],
24418
+ [19968, 42191],
24419
+ [43360, 43391],
24420
+ [44032, 55203],
24421
+ [63744, 64255],
24422
+ [65040, 65049],
24423
+ [65072, 65103],
24424
+ [65280, 65376],
24425
+ [65504, 65510],
24426
+ [94176, 94180],
24427
+ [94208, 101119],
24428
+ [110592, 111359],
24429
+ [126980, 126980],
24430
+ [127183, 127183],
24431
+ [127374, 127374],
24432
+ [127377, 127386],
24433
+ [127462, 127487],
24434
+ [127488, 127776],
24435
+ [127789, 127797],
24436
+ [127799, 127868],
24437
+ [127870, 127891],
24438
+ [127904, 127946],
24439
+ [127951, 127955],
24440
+ [127968, 127984],
24441
+ [127988, 127988],
24442
+ [127992, 128062],
24443
+ [128064, 128064],
24444
+ [128066, 128252],
24445
+ [128255, 128317],
24446
+ [128331, 128334],
24447
+ [128336, 128359],
24448
+ [128378, 128378],
24449
+ [128405, 128406],
24450
+ [128420, 128420],
24451
+ [128507, 128591],
24452
+ [128640, 128709],
24453
+ [128716, 128716],
24454
+ [128720, 128722],
24455
+ [128725, 128727],
24456
+ [128747, 128748],
24457
+ [128756, 128764],
24458
+ [128992, 129003],
24459
+ [129292, 129338],
24460
+ [129340, 129349],
24461
+ [129351, 129535],
24462
+ [129648, 129660],
24463
+ [129664, 129673],
24464
+ [129679, 129734],
24465
+ [129742, 129756],
24466
+ [129759, 129769],
24467
+ [129776, 129784],
24468
+ [131072, 196605],
24469
+ [196608, 262141]
24470
+ ];
24471
+ /** Whether one code point falls inside a wide range (binary search). */
24472
+ function isWideCodePoint(code) {
24473
+ let low = 0;
24474
+ let high = WIDE_RANGES.length - 1;
24475
+ while (low <= high) {
24476
+ const mid = low + high >> 1;
24477
+ const [start, end] = WIDE_RANGES[mid];
24478
+ if (code < start) high = mid - 1;
24479
+ else if (code > end) low = mid + 1;
24480
+ else return true;
24481
+ }
24482
+ return false;
24483
+ }
24484
+ /** Zero-width code points: combining marks, enclosing marks, format controls. */
24485
+ const ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
24486
+ /** Module-cached grapheme segmenter; undefined when the runtime lacks it. */
24487
+ let segmenter$1;
24488
+ try {
24489
+ segmenter$1 = new Intl.Segmenter("en", { granularity: "grapheme" });
24490
+ } catch {
24491
+ segmenter$1 = void 0;
24492
+ }
24493
+ /** Split text into grapheme clusters (code points when Segmenter is absent). */
24494
+ function splitGraphemes$1(text) {
24495
+ if (segmenter$1 === void 0) return Array.from(text);
24496
+ return Array.from(segmenter$1.segment(text), (part) => part.segment);
24497
+ }
24498
+ /** Terminal-cell width of one grapheme cluster. */
24499
+ function graphemeWidth(cluster) {
24500
+ if (cluster.includes("️", 0)) return 2;
24501
+ const first = cluster.codePointAt(0) ?? 0;
24502
+ if (ZERO_WIDTH.test(String.fromCodePoint(first))) return 0;
24503
+ return isWideCodePoint(first) ? 2 : 1;
24504
+ }
24505
+ /**
24506
+ * Terminal-cell width of a string: an ASCII fast path avoids the segmenter
24507
+ * for the overwhelmingly common case; everything else sums grapheme clusters.
24508
+ * Control characters occupy no cells (display sanitization makes them
24509
+ * visible escapes before they ever reach a budget).
24510
+ * @param text - display-safe or raw text to measure.
24511
+ * @returns the column count the terminal will draw.
24512
+ */
24513
+ function stringWidth(text) {
24514
+ let asciiOnly = true;
24515
+ for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) > 127) {
24516
+ asciiOnly = false;
24517
+ break;
24518
+ }
24519
+ if (asciiOnly) {
24520
+ let columns = 0;
24521
+ for (let index = 0; index < text.length; index += 1) {
24522
+ const code = text.charCodeAt(index);
24523
+ if (code >= 32 && code !== 127) columns += 1;
24524
+ }
24525
+ return columns;
24526
+ }
24527
+ let columns = 0;
24528
+ for (const cluster of splitGraphemes$1(text)) columns += graphemeWidth(cluster);
24529
+ return columns;
24530
+ }
24531
+ //#endregion
24358
24532
  //#region src/render/text.ts
24359
24533
  /**
24360
24534
  * Display-boundary sanitization for externally sourced text (model output,
@@ -24395,56 +24569,45 @@ function displayText(text) {
24395
24569
  function singleLineText(text) {
24396
24570
  return displayText(text).replace(/\r?\n/gu, " ↵ ").replace(/\t/gu, " ");
24397
24571
  }
24398
- /** Terminal-cell width matching the TUI's existing CJK-aware wrapping rule. */
24399
- function cellWidth(text) {
24400
- let columns = 0;
24401
- for (const char of text) columns += (char.codePointAt(0) ?? 0) > 11903 ? 2 : 1;
24402
- return columns;
24403
- }
24404
24572
  /**
24405
24573
  * Truncate one display-safe row without ever exceeding its physical-column
24406
24574
  * budget. The ellipsis is included inside the budget, matching Codex's popup
24407
- * truncation contract; the previous app-local helper appended it after the
24408
- * row was already full and could force an extra terminal wrap.
24575
+ * truncation contract; the cut walks grapheme clusters so emoji and
24576
+ * combining sequences never split mid-cluster.
24409
24577
  */
24410
24578
  function truncateColumns(text, columns) {
24411
24579
  const limit = Math.max(0, Math.floor(columns));
24412
24580
  if (limit === 0) return "";
24413
- if (cellWidth(text) <= limit) return text;
24581
+ if (stringWidth(text) <= limit) return text;
24414
24582
  const contentLimit = limit - 1;
24415
24583
  let used = 0;
24416
24584
  let result = "";
24417
- for (const char of text) {
24418
- const width = cellWidth(char);
24585
+ for (const cluster of splitGraphemes$1(text)) {
24586
+ const width = graphemeWidth(cluster);
24419
24587
  if (used + width > contentLimit) break;
24420
- result += char;
24588
+ result += cluster;
24421
24589
  used += width;
24422
24590
  }
24423
24591
  return `${result}…`;
24424
24592
  }
24425
- /** Read one Unicode character immediately before `end`. */
24426
- function previousCharacter(text, end) {
24427
- const last = text.charCodeAt(end - 1);
24428
- if (last >= 56320 && last <= 57343 && end >= 2) {
24429
- const first = text.charCodeAt(end - 2);
24430
- if (first >= 55296 && first <= 56319) return {
24431
- char: text.slice(end - 2, end),
24432
- start: end - 2
24433
- };
24434
- }
24435
- return {
24436
- char: text.slice(end - 1, end),
24437
- start: end - 1
24438
- };
24439
- }
24593
+ /** Punctuation that must never START a physical row (CJK kinsoku tail set). */
24594
+ const ROW_START_FORBIDDEN = ",。、;:!?)】」』〉》…‥";
24595
+ /** Punctuation that must never END a physical row (CJK kinsoku head set). */
24596
+ const ROW_END_FORBIDDEN = "(【「『〈《";
24440
24597
  /**
24441
- * Keep only the newest display-safe text that fits a terminal rectangle.
24442
- * The scan walks backward and stops as soon as the suffix is full, so a long
24443
- * reasoning stream does not rescan its entire accumulated prefix per chunk.
24444
- * Explicit newlines and terminal wrapping both consume rows; tabs expand to
24445
- * two spaces so terminal tab stops (which render at contextual column 8
24446
- * boundaries, not at the budgeted cell count) cannot inflate the physical
24447
- * row count of the live region.
24598
+ * Keep the newest display-safe text that fits a terminal rectangle, wrapping
24599
+ * FORWARD from the start of the text and slicing the tail rows.
24600
+ *
24601
+ * Forward wrapping is what keeps a streaming tail calm: rows already produced
24602
+ * never re-wrap as tokens append (a backward scan recomputes every wrap point
24603
+ * per chunk and the whole visible block jumps), and the wrap rules match the
24604
+ * settled text's renderer so the flush at turn end does not reflow the block
24605
+ * a second time. CJK kinsoku applies at both edges: closing punctuation
24606
+ * overhangs up to two cells onto the filled row instead of starting the next
24607
+ * one (within the caret column the caller reserves), and opening punctuation
24608
+ * moves down instead of dangling at a row end. Tabs expand to two spaces so
24609
+ * terminal tab stops cannot inflate the physical row count; clusters carry
24610
+ * emoji presentation and combining marks whole.
24448
24611
  * @param text - raw externally sourced text.
24449
24612
  * @param columns - available terminal columns.
24450
24613
  * @param rows - available terminal rows.
@@ -24453,38 +24616,43 @@ function previousCharacter(text, end) {
24453
24616
  function displayTail(text, columns, rows) {
24454
24617
  const columnLimit = Math.max(1, Math.floor(columns));
24455
24618
  const rowLimit = Math.max(1, Math.floor(rows));
24456
- const reversed = [];
24457
- let row = 1;
24619
+ const wrapped = [];
24620
+ let current = "";
24458
24621
  let used = 0;
24459
- let end = text.length;
24460
- while (end > 0) {
24461
- const previous = previousCharacter(text, end);
24462
- if (previous.char === "\n") {
24463
- if (row >= rowLimit) break;
24464
- reversed.push("\n");
24465
- row += 1;
24466
- used = 0;
24467
- end = previous.start;
24622
+ let lastCluster = "";
24623
+ const flush = () => {
24624
+ wrapped.push(current);
24625
+ current = "";
24626
+ used = 0;
24627
+ lastCluster = "";
24628
+ };
24629
+ for (const cluster of splitGraphemes$1(text)) {
24630
+ if (cluster === "\n") {
24631
+ flush();
24468
24632
  continue;
24469
24633
  }
24470
- const safe = previous.char === " " ? " " : displayText(previous.char);
24471
- const width = cellWidth(safe);
24634
+ const safe = cluster === " " ? " " : displayText(cluster);
24635
+ const width = stringWidth(safe);
24472
24636
  if (used > 0 && used + width > columnLimit) {
24473
- if (row >= rowLimit) break;
24474
- reversed.push("\n");
24475
- row += 1;
24476
- used = 0;
24477
- }
24478
- const extraRows = Math.floor(Math.max(0, width - 1) / columnLimit);
24479
- if (row + extraRows > rowLimit) break;
24480
- row += extraRows;
24481
- reversed.push(safe);
24482
- used = extraRows === 0 ? used + width : width - extraRows * columnLimit;
24483
- end = previous.start;
24637
+ if (!(width <= 2 && ROW_START_FORBIDDEN.includes(cluster))) {
24638
+ if (lastCluster !== "" && ROW_END_FORBIDDEN.includes(lastCluster)) {
24639
+ const carried = lastCluster;
24640
+ current = current.slice(0, current.length - carried.length);
24641
+ flush();
24642
+ current = carried;
24643
+ used = stringWidth(carried);
24644
+ } else flush();
24645
+ }
24646
+ }
24647
+ current += safe;
24648
+ used += width;
24649
+ lastCluster = cluster;
24484
24650
  }
24651
+ if (current !== "" || wrapped.length > 0 && text.endsWith("\n")) flush();
24652
+ const truncated = wrapped.length > rowLimit;
24485
24653
  return {
24486
- text: reversed.reverse().join(""),
24487
- truncated: end > 0
24654
+ text: (truncated ? wrapped.slice(-rowLimit) : wrapped).join("\n"),
24655
+ truncated
24488
24656
  };
24489
24657
  }
24490
24658
  //#endregion
@@ -24947,535 +25115,14 @@ function reasoningOf(content) {
24947
25115
  function estimateTokens(text) {
24948
25116
  let wide = 0;
24949
25117
  let narrow = 0;
24950
- for (const char of text) if ((char.codePointAt(0) ?? 0) > 11903) wide += 1;
25118
+ for (const cluster of splitGraphemes$1(text)) if (graphemeWidth(cluster) > 1) wide += 1;
24951
25119
  else narrow += 1;
24952
25120
  return wide + Math.ceil(narrow / 4);
24953
25121
  }
24954
- /** A fresh, empty transcript view. */
24955
- function createTranscriptView() {
24956
- return {
24957
- entries: [],
24958
- streaming: "",
24959
- streamingReasoning: "",
24960
- todos: [],
24961
- toolCallOrdinal: 0,
24962
- busy: false,
24963
- busySince: 0,
24964
- model: "",
24965
- plan: false,
24966
- permission: "",
24967
- title: "",
24968
- sandbox: "",
24969
- goal: void 0,
24970
- pending: {
24971
- "next-turn": [],
24972
- "next-step": []
24973
- },
24974
- stats: {
24975
- turns: 0,
24976
- steps: 0,
24977
- llmMs: 0,
24978
- toolMs: 0,
24979
- usage: {
24980
- inputTokens: 0,
24981
- outputTokens: 0,
24982
- cacheReadTokens: 0
24983
- },
24984
- lastPromptTokens: 0,
24985
- contextWindow: 0,
24986
- contextSegments: {
24987
- system: 0,
24988
- prompt: 0,
24989
- assistant: 0,
24990
- thinking: 0,
24991
- tools: 0
24992
- },
24993
- ttftMs: 0,
24994
- ttftSteps: 0,
24995
- decodeMs: 0,
24996
- decodeTokens: 0,
24997
- reasoningEffort: ""
24998
- },
24999
- anchors: {
25000
- stepStart: /* @__PURE__ */ new Map(),
25001
- toolStart: /* @__PURE__ */ new Map(),
25002
- firstChunkAt: /* @__PURE__ */ new Map(),
25003
- compactionTokens: /* @__PURE__ */ new Map(),
25004
- lastPruneTokens: 0,
25005
- turnFiles: /* @__PURE__ */ new Map(),
25006
- turnSteps: /* @__PURE__ */ new Map(),
25007
- turnTools: /* @__PURE__ */ new Map()
25008
- }
25009
- };
25010
- }
25011
25122
  /** Full prompt text of a queued message (identical to the durable user row it retires into). */
25012
25123
  function pendingText(content) {
25013
25124
  return textOf(content);
25014
25125
  }
25015
- /**
25016
- * Fold one session event into an updated view (copy-on-write).
25017
- * @param view - the view before the event.
25018
- * @param event - one durable session event from `session/event` or the log.
25019
- * @returns the view after the event; the input view is never mutated.
25020
- */
25021
- function projectEvent(view, event) {
25022
- switch (event.type) {
25023
- case "user/message": {
25024
- const message = event.data;
25025
- let entries = view.entries;
25026
- let pending = view.pending;
25027
- for (const target of ["next-turn", "next-step"]) {
25028
- const index = pending[target].indexOf(message.id);
25029
- if (index < 0) continue;
25030
- pending = {
25031
- ...pending,
25032
- [target]: pending[target].filter((_, i) => i !== index)
25033
- };
25034
- entries = entries.filter((entry) => !(entry.kind === "pending" && entry.messageId === message.id));
25035
- }
25036
- const text = textOf(message.content);
25037
- const images = imagesOf(message.content);
25038
- if (message.source.kind === "user") return {
25039
- ...view,
25040
- pending,
25041
- entries: [...entries, {
25042
- kind: "user",
25043
- text,
25044
- notice: false,
25045
- ...images.length === 0 ? {} : { images }
25046
- }],
25047
- stats: {
25048
- ...view.stats,
25049
- contextSegments: {
25050
- ...view.stats.contextSegments,
25051
- prompt: view.stats.contextSegments.prompt + estimateTokens(text)
25052
- }
25053
- }
25054
- };
25055
- const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
25056
- const summary = boundContextSummary(notice);
25057
- return {
25058
- ...view,
25059
- pending,
25060
- entries: [...entries, {
25061
- kind: "user",
25062
- text: summary,
25063
- notice: true
25064
- }],
25065
- stats: {
25066
- ...view.stats,
25067
- contextSegments: {
25068
- ...view.stats.contextSegments,
25069
- system: view.stats.contextSegments.system + estimateTokens(summary)
25070
- }
25071
- }
25072
- };
25073
- }
25074
- case "agent/inbox/spliced": {
25075
- const { target, start, removedCount = 0, inserted } = event.data;
25076
- const ids = view.pending[target];
25077
- const removed = ids.slice(start, start + removedCount);
25078
- const nextIds = [
25079
- ...ids.slice(0, start),
25080
- ...ids.slice(start + removedCount),
25081
- ...inserted.map((message) => message.id)
25082
- ];
25083
- let entries = view.entries;
25084
- if (removed.length > 0) {
25085
- const removedSet = new Set(removed);
25086
- entries = entries.filter((entry) => !(entry.kind === "pending" && entry.target === target && removedSet.has(entry.messageId)));
25087
- }
25088
- for (const message of inserted) entries = [...entries, {
25089
- kind: "pending",
25090
- messageId: message.id,
25091
- target,
25092
- text: pendingText(message.content),
25093
- ...imagesOf(message.content).length === 0 ? {} : { images: imagesOf(message.content) }
25094
- }];
25095
- return {
25096
- ...view,
25097
- entries,
25098
- pending: {
25099
- ...view.pending,
25100
- [target]: nextIds
25101
- }
25102
- };
25103
- }
25104
- case "assistant/chunk": {
25105
- const chunk = event.data.chunk;
25106
- const key = `${event.data.turn}:${event.data.step}`;
25107
- const delta = chunk.type === "text-delta" || chunk.type === "reasoning-delta" ? chunk.text : "";
25108
- let stats = view.stats;
25109
- if (delta !== "" && !view.anchors.firstChunkAt.has(key)) {
25110
- view.anchors.firstChunkAt.set(key, event.time);
25111
- const started = view.anchors.stepStart.get(key);
25112
- if (started !== void 0) stats = {
25113
- ...stats,
25114
- ttftMs: stats.ttftMs + Math.max(0, event.time - started),
25115
- ttftSteps: stats.ttftSteps + 1
25116
- };
25117
- }
25118
- if (chunk.type === "text-delta") return {
25119
- ...view,
25120
- streaming: appendStreamingTail(view.streaming, chunk.text),
25121
- stats
25122
- };
25123
- if (chunk.type === "reasoning-delta") return {
25124
- ...view,
25125
- streamingReasoning: appendStreamingTail(view.streamingReasoning, chunk.text),
25126
- stats
25127
- };
25128
- return view;
25129
- }
25130
- case "assistant/message": {
25131
- const key = `${event.data.turn}:${event.data.step}`;
25132
- const started = view.anchors.stepStart.get(key);
25133
- view.anchors.stepStart.delete(key);
25134
- const firstChunk = view.anchors.firstChunkAt.get(key);
25135
- view.anchors.firstChunkAt.delete(key);
25136
- if (view.anchors.turnSteps.get(event.data.turn) === key) view.anchors.turnSteps.delete(event.data.turn);
25137
- const usage = event.data.usage;
25138
- const totals = view.stats.usage;
25139
- const text = textOf(event.data.message.content);
25140
- const reasoning = reasoningOf(event.data.message.content);
25141
- return {
25142
- ...view,
25143
- streaming: "",
25144
- streamingReasoning: "",
25145
- entries: [...view.entries, {
25146
- kind: "assistant",
25147
- text,
25148
- reasoning,
25149
- interrupted: event.data.interrupted === true ? true : void 0
25150
- }],
25151
- stats: {
25152
- ...view.stats,
25153
- llmMs: view.stats.llmMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
25154
- usage: usage === void 0 ? totals : {
25155
- inputTokens: totals.inputTokens + usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
25156
- outputTokens: totals.outputTokens + usage.outputTokens,
25157
- cacheReadTokens: totals.cacheReadTokens + (usage.cacheReadTokens ?? 0)
25158
- },
25159
- lastPromptTokens: usage === void 0 ? view.stats.lastPromptTokens : usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
25160
- decodeMs: view.stats.decodeMs + (firstChunk === void 0 ? 0 : Math.max(0, event.time - firstChunk)),
25161
- decodeTokens: view.stats.decodeTokens + (firstChunk === void 0 || usage === void 0 ? 0 : usage.outputTokens),
25162
- contextSegments: {
25163
- ...view.stats.contextSegments,
25164
- thinking: view.stats.contextSegments.thinking + estimateTokens(reasoning),
25165
- assistant: view.stats.contextSegments.assistant + estimateTokens(text)
25166
- }
25167
- }
25168
- };
25169
- }
25170
- case "tool/call": {
25171
- const data = event.data;
25172
- view.anchors.toolStart.set(data.callId, event.time);
25173
- const turnTools = view.anchors.turnTools.get(data.turn) ?? /* @__PURE__ */ new Set();
25174
- turnTools.add(data.callId);
25175
- view.anchors.turnTools.set(data.turn, turnTools);
25176
- const ordinal = view.toolCallOrdinal + 1;
25177
- return {
25178
- ...view,
25179
- toolCallOrdinal: ordinal,
25180
- entries: [...view.entries, {
25181
- kind: "tool",
25182
- callId: data.callId,
25183
- ordinal,
25184
- name: data.name,
25185
- arguments: data.arguments,
25186
- preview: toolArgumentsPreview(data.arguments, data.name),
25187
- prompt: toolPromptPreview(data.name, data.arguments),
25188
- state: "running",
25189
- summary: "",
25190
- detail: void 0
25191
- }],
25192
- stats: {
25193
- ...view.stats,
25194
- contextSegments: {
25195
- ...view.stats.contextSegments,
25196
- tools: view.stats.contextSegments.tools + (typeof data.arguments === "string" ? estimateTokens(data.arguments) : 0)
25197
- }
25198
- }
25199
- };
25200
- }
25201
- case "tool/result": {
25202
- const block = event.data.message.content[0];
25203
- const started = view.anchors.toolStart.get(block.toolCallId);
25204
- view.anchors.toolStart.delete(block.toolCallId);
25205
- const turnTools = view.anchors.turnTools.get(event.data.turn);
25206
- if (turnTools !== void 0) {
25207
- turnTools.delete(block.toolCallId);
25208
- if (turnTools.size === 0) view.anchors.turnTools.delete(event.data.turn);
25209
- }
25210
- const rawText = textOf(block.content);
25211
- const summary = boundContextSummary(rawText);
25212
- const detail = toolResultDetail(event.data.meta, rawText);
25213
- if (detail?.kind === "diff") {
25214
- const set = view.anchors.turnFiles.get(event.data.turn) ?? /* @__PURE__ */ new Set();
25215
- for (const diff of detail.diffs) set.add(diff.path);
25216
- view.anchors.turnFiles.set(event.data.turn, set);
25217
- }
25218
- const entries = view.entries.map((entry) => {
25219
- if (entry.kind !== "tool" || entry.callId !== block.toolCallId) return entry;
25220
- return {
25221
- ...entry,
25222
- state: block.isError === true ? "error" : "done",
25223
- summary,
25224
- detail
25225
- };
25226
- });
25227
- return {
25228
- ...view,
25229
- entries,
25230
- stats: {
25231
- ...view.stats,
25232
- toolMs: view.stats.toolMs + (started === void 0 ? 0 : Math.max(0, event.time - started)),
25233
- contextSegments: {
25234
- ...view.stats.contextSegments,
25235
- tools: view.stats.contextSegments.tools + estimateTokens(rawText)
25236
- }
25237
- }
25238
- };
25239
- }
25240
- case "todo/write": return {
25241
- ...view,
25242
- todos: event.data.todos
25243
- };
25244
- case "turn/start": return {
25245
- ...view,
25246
- busy: true,
25247
- busySince: view.busy ? view.busySince : event.time,
25248
- todos: [],
25249
- stats: {
25250
- ...view.stats,
25251
- turns: view.stats.turns + 1
25252
- }
25253
- };
25254
- case "step/start": {
25255
- const key = `${event.data.turn}:${event.data.step}`;
25256
- const previous = view.anchors.turnSteps.get(event.data.turn);
25257
- if (previous !== void 0 && previous !== key) {
25258
- view.anchors.stepStart.delete(previous);
25259
- view.anchors.firstChunkAt.delete(previous);
25260
- }
25261
- view.anchors.turnSteps.set(event.data.turn, key);
25262
- view.anchors.stepStart.set(key, event.time);
25263
- return {
25264
- ...view,
25265
- streaming: "",
25266
- streamingReasoning: "",
25267
- stats: {
25268
- ...view.stats,
25269
- steps: view.stats.steps + 1
25270
- }
25271
- };
25272
- }
25273
- case "turn/end": {
25274
- const reason = event.data.reason;
25275
- const appended = [];
25276
- if (reason.kind === "error") {
25277
- const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · open /model to add an API key" : "";
25278
- appended.push({
25279
- kind: "error",
25280
- text: `${reason.error.code}: ${reason.error.message}${recovery}`
25281
- });
25282
- } else {
25283
- const marker = reason.kind === "aborted" ? reason.reason.kind === "user" ? "turn cancelled by the user" : `turn cancelled (${reason.reason.kind})` : reason.kind === "max-tokens" ? "turn hit the output-token ceiling (max-tokens)" : reason.kind === "blocked" ? "turn ended blocked" : reason.kind === "interrupted" ? "turn was interrupted by a restart" : void 0;
25284
- if (marker !== void 0) appended.push({
25285
- kind: "turn-marker",
25286
- text: marker
25287
- });
25288
- }
25289
- const files = view.anchors.turnFiles.get(event.data.turn);
25290
- view.anchors.turnFiles.delete(event.data.turn);
25291
- if (files !== void 0 && files.size > 0) appended.push({
25292
- kind: "files",
25293
- paths: [...files].slice(0, 12)
25294
- });
25295
- const stepKey = view.anchors.turnSteps.get(event.data.turn);
25296
- if (stepKey !== void 0) {
25297
- view.anchors.stepStart.delete(stepKey);
25298
- view.anchors.firstChunkAt.delete(stepKey);
25299
- view.anchors.turnSteps.delete(event.data.turn);
25300
- }
25301
- const turnToolSet = view.anchors.turnTools.get(event.data.turn);
25302
- if (turnToolSet !== void 0) {
25303
- for (const callId of turnToolSet) view.anchors.toolStart.delete(callId);
25304
- view.anchors.turnTools.delete(event.data.turn);
25305
- }
25306
- if (appended.length === 0) return {
25307
- ...view,
25308
- busy: false,
25309
- busySince: 0,
25310
- streaming: "",
25311
- streamingReasoning: ""
25312
- };
25313
- return {
25314
- ...view,
25315
- busy: false,
25316
- busySince: 0,
25317
- streaming: "",
25318
- streamingReasoning: "",
25319
- entries: [...view.entries, ...appended]
25320
- };
25321
- }
25322
- case "llm/retry": {
25323
- const data = event.data;
25324
- return {
25325
- ...view,
25326
- streaming: "",
25327
- streamingReasoning: "",
25328
- entries: [...view.entries, {
25329
- kind: "retry",
25330
- retryId: data.retryId,
25331
- attempt: data.retry,
25332
- max: "maxRetries" in data ? data.maxRetries : data.retry,
25333
- code: data.failure.code,
25334
- delayMs: data.delayMs,
25335
- state: "running"
25336
- }]
25337
- };
25338
- }
25339
- case "llm/retry-started": {
25340
- const data = event.data;
25341
- const entries = view.entries.map((entry) => {
25342
- if (entry.kind !== "retry" || entry.retryId !== data.retryId) return entry;
25343
- return {
25344
- ...entry,
25345
- state: "done"
25346
- };
25347
- });
25348
- return {
25349
- ...view,
25350
- entries
25351
- };
25352
- }
25353
- case "sandbox/mode": return {
25354
- ...view,
25355
- sandbox: event.data.mode
25356
- };
25357
- case "goal/change": {
25358
- const data = event.data;
25359
- const clip = (text) => text.length > 60 ? `${text.slice(0, 59)}…` : text;
25360
- if (data.operation === "clear") return {
25361
- ...view,
25362
- goal: void 0,
25363
- entries: [...view.entries, {
25364
- kind: "turn-marker",
25365
- text: "◎ goal cleared"
25366
- }]
25367
- };
25368
- const goal = {
25369
- objective: data.goal.objective,
25370
- phase: data.goal.phase,
25371
- rounds: data.roundsStarted,
25372
- max: data.goal.maxGoalRounds,
25373
- blocked: data.goal.blockedReason?.message ?? ""
25374
- };
25375
- const line = data.operation === "create" ? `◎ goal: ${clip(data.goal.objective)}` : data.operation === "complete" ? "◎ goal complete" : data.operation === "pause" ? "◎ goal paused" : data.operation === "resume" ? "◎ goal resumed" : data.operation === "block" ? `◎ goal blocked: ${clip(goal.blocked)}` : void 0;
25376
- return {
25377
- ...view,
25378
- goal,
25379
- entries: line === void 0 ? view.entries : [...view.entries, {
25380
- kind: "turn-marker",
25381
- text: line
25382
- }]
25383
- };
25384
- }
25385
- case "session/title": return {
25386
- ...view,
25387
- title: event.data.title
25388
- };
25389
- case "compaction/summary":
25390
- if (view.anchors.compactionTokens.size >= MAX_COMPACTION_SUMMARY_RESIDUE) {
25391
- const oldest = view.anchors.compactionTokens.keys().next().value;
25392
- if (oldest !== void 0) view.anchors.compactionTokens.delete(oldest);
25393
- }
25394
- view.anchors.compactionTokens.set(event.data.compactionId, event.data.shadowedTokenCount);
25395
- return view;
25396
- case "compaction/prune": return {
25397
- ...view,
25398
- anchors: {
25399
- ...view.anchors,
25400
- lastPruneTokens: event.data.shadowedTokenCount
25401
- }
25402
- };
25403
- case "compaction/end": {
25404
- const ok = event.data.error === void 0;
25405
- const tokens = view.anchors.compactionTokens.get(event.data.compactionId) ?? view.anchors.lastPruneTokens;
25406
- view.anchors.compactionTokens.delete(event.data.compactionId);
25407
- return {
25408
- ...view,
25409
- entries: [...view.entries, {
25410
- kind: "compaction",
25411
- ok,
25412
- tokens,
25413
- error: event.data.error ?? ""
25414
- }]
25415
- };
25416
- }
25417
- case "request/context": return {
25418
- ...view,
25419
- stats: {
25420
- ...view.stats,
25421
- contextWindow: event.data.contextWindow ?? view.stats.contextWindow
25422
- }
25423
- };
25424
- case "request/header": {
25425
- const config = event.data.header.config;
25426
- return {
25427
- ...view,
25428
- model: `${config.provider}/${config.model}`,
25429
- stats: {
25430
- ...view.stats,
25431
- reasoningEffort: config.reasoningEffort === void 0 ? "" : String(config.reasoningEffort),
25432
- contextSegments: {
25433
- ...view.stats.contextSegments,
25434
- system: estimateTokens(event.data.header.system ?? "")
25435
- }
25436
- }
25437
- };
25438
- }
25439
- case "plan/mode": return {
25440
- ...view,
25441
- plan: event.data.active
25442
- };
25443
- case "permission/preset": return {
25444
- ...view,
25445
- permission: event.data.preset
25446
- };
25447
- case "command/run": {
25448
- const data = event.data;
25449
- return {
25450
- ...view,
25451
- entries: [...view.entries, {
25452
- kind: "command",
25453
- commandId: data.commandId,
25454
- name: data.name,
25455
- args: data.args ?? "",
25456
- state: "running",
25457
- summary: ""
25458
- }]
25459
- };
25460
- }
25461
- case "command/done": {
25462
- const data = event.data;
25463
- const entries = view.entries.map((entry) => {
25464
- if (entry.kind !== "command" || entry.commandId !== data.commandId) return entry;
25465
- return {
25466
- ...entry,
25467
- state: data.kind === "success" ? "done" : "error",
25468
- summary: boundContextSummary(data.text ?? "")
25469
- };
25470
- });
25471
- return {
25472
- ...view,
25473
- entries
25474
- };
25475
- }
25476
- default: return view;
25477
- }
25478
- }
25479
25126
  /** @internal A fresh replay accumulator whose state mirrors `createTranscriptView()`. */
25480
25127
  function createReplayAccumulator() {
25481
25128
  return {
@@ -25586,6 +25233,9 @@ function retireReplayEntry(acc, index) {
25586
25233
  * to a sequential fold; only the `entries` container operations are mutable.
25587
25234
  *
25588
25235
  * @internal Test-instrumentation path; `projectEvents` is the public entry.
25236
+ * @returns whether the event changed the accumulated state — the live store
25237
+ * stays silent and keeps its snapshot identity for ignored events, exactly
25238
+ * like the copy-on-write reducer returning its input view unchanged.
25589
25239
  */
25590
25240
  function replayProjectEvent(acc, event) {
25591
25241
  switch (event.type) {
@@ -25620,7 +25270,7 @@ function replayProjectEvent(acc, event) {
25620
25270
  prompt: acc.stats.contextSegments.prompt + estimateTokens(text)
25621
25271
  }
25622
25272
  };
25623
- return;
25273
+ return true;
25624
25274
  }
25625
25275
  const notice = message.source.kind === "plugin" && message.source.form === "notice" ? message.source.summary : message.source.kind;
25626
25276
  const summary = boundContextSummary(notice);
@@ -25636,7 +25286,7 @@ function replayProjectEvent(acc, event) {
25636
25286
  system: acc.stats.contextSegments.system + estimateTokens(summary)
25637
25287
  }
25638
25288
  };
25639
- return;
25289
+ return true;
25640
25290
  }
25641
25291
  case "agent/inbox/spliced": {
25642
25292
  const { target, start, removedCount = 0, inserted } = event.data;
@@ -25666,7 +25316,7 @@ function replayProjectEvent(acc, event) {
25666
25316
  ids.push(message.id);
25667
25317
  acc.ops += 1;
25668
25318
  }
25669
- return;
25319
+ return true;
25670
25320
  }
25671
25321
  case "assistant/chunk": {
25672
25322
  const chunk = event.data.chunk;
@@ -25682,13 +25332,13 @@ function replayProjectEvent(acc, event) {
25682
25332
  }
25683
25333
  if (chunk.type === "text-delta") {
25684
25334
  acc.streaming = appendStreamingTail(acc.streaming, chunk.text);
25685
- return;
25335
+ return true;
25686
25336
  }
25687
25337
  if (chunk.type === "reasoning-delta") {
25688
25338
  acc.streamingReasoning = appendStreamingTail(acc.streamingReasoning, chunk.text);
25689
- return;
25339
+ return true;
25690
25340
  }
25691
- return;
25341
+ return false;
25692
25342
  }
25693
25343
  case "assistant/message": {
25694
25344
  const key = `${event.data.turn}:${event.data.step}`;
@@ -25726,7 +25376,7 @@ function replayProjectEvent(acc, event) {
25726
25376
  assistant: acc.stats.contextSegments.assistant + estimateTokens(text)
25727
25377
  }
25728
25378
  };
25729
- return;
25379
+ return true;
25730
25380
  }
25731
25381
  case "tool/call": {
25732
25382
  const data = event.data;
@@ -25755,7 +25405,7 @@ function replayProjectEvent(acc, event) {
25755
25405
  tools: acc.stats.contextSegments.tools + (typeof data.arguments === "string" ? estimateTokens(data.arguments) : 0)
25756
25406
  }
25757
25407
  };
25758
- return;
25408
+ return true;
25759
25409
  }
25760
25410
  case "tool/result": {
25761
25411
  const block = event.data.message.content[0];
@@ -25789,11 +25439,11 @@ function replayProjectEvent(acc, event) {
25789
25439
  tools: acc.stats.contextSegments.tools + estimateTokens(rawText)
25790
25440
  }
25791
25441
  };
25792
- return;
25442
+ return true;
25793
25443
  }
25794
25444
  case "todo/write":
25795
25445
  acc.todos = event.data.todos;
25796
- return;
25446
+ return true;
25797
25447
  case "turn/start": {
25798
25448
  const wasBusy = acc.busy;
25799
25449
  acc.busy = true;
@@ -25803,7 +25453,7 @@ function replayProjectEvent(acc, event) {
25803
25453
  ...acc.stats,
25804
25454
  turns: acc.stats.turns + 1
25805
25455
  };
25806
- return;
25456
+ return true;
25807
25457
  }
25808
25458
  case "step/start": {
25809
25459
  const key = `${event.data.turn}:${event.data.step}`;
@@ -25820,7 +25470,7 @@ function replayProjectEvent(acc, event) {
25820
25470
  ...acc.stats,
25821
25471
  steps: acc.stats.steps + 1
25822
25472
  };
25823
- return;
25473
+ return true;
25824
25474
  }
25825
25475
  case "turn/end": {
25826
25476
  const reason = event.data.reason;
@@ -25860,7 +25510,7 @@ function replayProjectEvent(acc, event) {
25860
25510
  acc.busy = false;
25861
25511
  acc.busySince = 0;
25862
25512
  for (const entry of appended) appendReplayEntry(acc, entry);
25863
- return;
25513
+ return true;
25864
25514
  }
25865
25515
  case "llm/retry": {
25866
25516
  const data = event.data;
@@ -25876,7 +25526,7 @@ function replayProjectEvent(acc, event) {
25876
25526
  state: "running"
25877
25527
  });
25878
25528
  indexList(acc.retryIndex, data.retryId).push(acc.entries.length - 1);
25879
- return;
25529
+ return true;
25880
25530
  }
25881
25531
  case "llm/retry-started": {
25882
25532
  const data = event.data;
@@ -25884,11 +25534,11 @@ function replayProjectEvent(acc, event) {
25884
25534
  ...entry,
25885
25535
  state: "done"
25886
25536
  }));
25887
- return;
25537
+ return true;
25888
25538
  }
25889
25539
  case "sandbox/mode":
25890
25540
  acc.sandbox = event.data.mode;
25891
- return;
25541
+ return true;
25892
25542
  case "goal/change": {
25893
25543
  const data = event.data;
25894
25544
  const clip = (text) => text.length > 60 ? `${text.slice(0, 59)}…` : text;
@@ -25898,7 +25548,7 @@ function replayProjectEvent(acc, event) {
25898
25548
  kind: "turn-marker",
25899
25549
  text: "◎ goal cleared"
25900
25550
  });
25901
- return;
25551
+ return true;
25902
25552
  }
25903
25553
  const goal = {
25904
25554
  objective: data.goal.objective,
@@ -25913,21 +25563,21 @@ function replayProjectEvent(acc, event) {
25913
25563
  kind: "turn-marker",
25914
25564
  text: line
25915
25565
  });
25916
- return;
25566
+ return true;
25917
25567
  }
25918
25568
  case "session/title":
25919
25569
  acc.title = event.data.title;
25920
- return;
25570
+ return true;
25921
25571
  case "compaction/summary":
25922
25572
  if (acc.compactionTokens.size >= MAX_COMPACTION_SUMMARY_RESIDUE) {
25923
25573
  const oldest = acc.compactionTokens.keys().next().value;
25924
25574
  if (oldest !== void 0) acc.compactionTokens.delete(oldest);
25925
25575
  }
25926
25576
  acc.compactionTokens.set(event.data.compactionId, event.data.shadowedTokenCount);
25927
- return;
25577
+ return true;
25928
25578
  case "compaction/prune":
25929
25579
  acc.lastPruneTokens = event.data.shadowedTokenCount;
25930
- return;
25580
+ return true;
25931
25581
  case "compaction/end": {
25932
25582
  const ok = event.data.error === void 0;
25933
25583
  const tokens = acc.compactionTokens.get(event.data.compactionId) ?? acc.lastPruneTokens;
@@ -25938,14 +25588,14 @@ function replayProjectEvent(acc, event) {
25938
25588
  tokens,
25939
25589
  error: event.data.error ?? ""
25940
25590
  });
25941
- return;
25591
+ return true;
25942
25592
  }
25943
25593
  case "request/context":
25944
25594
  acc.stats = {
25945
25595
  ...acc.stats,
25946
25596
  contextWindow: event.data.contextWindow ?? acc.stats.contextWindow
25947
25597
  };
25948
- return;
25598
+ return true;
25949
25599
  case "request/header": {
25950
25600
  const config = event.data.header.config;
25951
25601
  acc.model = `${config.provider}/${config.model}`;
@@ -25957,14 +25607,14 @@ function replayProjectEvent(acc, event) {
25957
25607
  system: estimateTokens(event.data.header.system ?? "")
25958
25608
  }
25959
25609
  };
25960
- return;
25610
+ return true;
25961
25611
  }
25962
25612
  case "plan/mode":
25963
25613
  acc.plan = event.data.active;
25964
- return;
25614
+ return true;
25965
25615
  case "permission/preset":
25966
25616
  acc.permission = event.data.preset;
25967
- return;
25617
+ return true;
25968
25618
  case "command/run": {
25969
25619
  const data = event.data;
25970
25620
  appendReplayEntry(acc, {
@@ -25976,7 +25626,7 @@ function replayProjectEvent(acc, event) {
25976
25626
  summary: ""
25977
25627
  });
25978
25628
  indexList(acc.commandIndex, data.commandId).push(acc.entries.length - 1);
25979
- return;
25629
+ return true;
25980
25630
  }
25981
25631
  case "command/done": {
25982
25632
  const data = event.data;
@@ -25986,20 +25636,27 @@ function replayProjectEvent(acc, event) {
25986
25636
  summary: boundContextSummary(data.text ?? "")
25987
25637
  });
25988
25638
  updateReplayById(acc, acc.commandIndex, data.commandId, (entry) => entry.commandId === data.commandId, update);
25989
- return;
25639
+ return true;
25990
25640
  }
25991
- default: return;
25641
+ default: return false;
25992
25642
  }
25993
25643
  }
25994
25644
  /**
25995
- * Materialize the accumulated fold as a `TranscriptView`, compacting any
25996
- * retired tombstones. The anchors maps are handed through as-is (their
25997
- * content is identical to a sequential fold's).
25645
+ * Materialize the accumulated fold as a fresh immutable snapshot for the
25646
+ * live store. Unlike {@link finishReplay} the one-shot replay entry, which
25647
+ * hands the accumulator's own arrays through because the accumulator is
25648
+ * discarded — every array a renderer can hold is copied here, so later
25649
+ * folds never mutate a snapshot already handed out. Same fields, same
25650
+ * tombstone compaction.
25998
25651
  *
25999
- * @internal Test-instrumentation path; `projectEvents` is the public entry.
25652
+ * @internal Live-store path; `projectEvents` is the public entry.
26000
25653
  */
26001
- function finishReplay(acc) {
26002
- const entries = acc.removedCount === 0 ? acc.entries : acc.entries.filter((entry) => entry !== void 0);
25654
+ function snapshotReplayView(acc) {
25655
+ return materializeReplayView(acc, true);
25656
+ }
25657
+ /** Field-for-field materialization; `copy` selects snapshot array isolation. */
25658
+ function materializeReplayView(acc, copy) {
25659
+ const entries = acc.removedCount === 0 ? copy ? [...acc.entries] : acc.entries : acc.entries.filter((entry) => entry !== void 0);
26003
25660
  if (acc.removedCount > 0) acc.ops += acc.entries.length;
26004
25661
  return {
26005
25662
  entries,
@@ -26033,22 +25690,6 @@ function finishReplay(acc) {
26033
25690
  };
26034
25691
  }
26035
25692
  /**
26036
- * Fold a replayed event history into one view.
26037
- *
26038
- * Folding is near-linear in the log size: the mutable replay accumulator
26039
- * appends in place and resolves id-keyed updates through index maps, so a
26040
- * long persisted session replays without the O(N²) copy-on-write rebuilds a
26041
- * naive sequential fold would incur. The result is identical to folding
26042
- * {@link projectEvent} per event in order.
26043
- * @param events - events in `seq` order.
26044
- * @returns the folded view.
26045
- */
26046
- function projectEvents(events) {
26047
- const acc = createReplayAccumulator();
26048
- for (const event of events) replayProjectEvent(acc, event);
26049
- return finishReplay(acc);
26050
- }
26051
- /**
26052
25693
  * The append-only flush boundary for a transcript view: the count of entries
26053
25694
  * no later event can remove. Entries at or beyond this index are mutable and
26054
25695
  * must stay in the live tree.
@@ -26077,6 +25718,16 @@ function settledEntryCount(entries) {
26077
25718
  }
26078
25719
  //#endregion
26079
25720
  //#region src/render/markdown.ts
25721
+ /**
25722
+ * Terminal markdown renderer for assistant replies: a pure GFM-subset
25723
+ * block/inline parser producing styled line segments the Ink renderer maps
25724
+ * to colored text. No ANSI here — the app owns color mapping, tests own the
25725
+ * structure. The subset mirrors what agent replies actually emit: headings,
25726
+ * emphasis, inline/fenced code, flat lists, blockquotes, links, rules, GFM
25727
+ * tables, and wrapped paragraphs. Unknown syntax degrades to plain text.
25728
+ *
25729
+ * @module @deepseek-ai/dsh-code/render/markdown
25730
+ */
26080
25731
  /** Plain segment helper. */
26081
25732
  function seg(text, style = "plain") {
26082
25733
  return {
@@ -26084,14 +25735,9 @@ function seg(text, style = "plain") {
26084
25735
  style
26085
25736
  };
26086
25737
  }
26087
- /** Visible width of a run in columns (CJK counts double). */
25738
+ /** Visible width of a run in columns (grapheme-cluster and emoji aware). */
26088
25739
  function visibleColumns(text) {
26089
- let columns = 0;
26090
- for (const char of text) {
26091
- const code = char.codePointAt(0) ?? 0;
26092
- columns += code > 11903 ? 2 : 1;
26093
- }
26094
- return columns;
25740
+ return stringWidth(text);
26095
25741
  }
26096
25742
  /** Punctuation that should not become the first visible glyph of a row. */
26097
25743
  function isClosingPunctuation(text) {
@@ -26633,11 +26279,7 @@ function renderMarkdown(text, width, options = {}) {
26633
26279
  }
26634
26280
  //#endregion
26635
26281
  //#region src/render/animations.ts
26636
- /**
26637
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
26638
- * ring trail clockwise around the eight outer positions, one braille glyph
26639
- * per step — 8 frames × 125ms = the web's 1s cycle.
26640
- */
26282
+ /** Original terminal StateDot chase frames. */
26641
26283
  const BUSY_CHASE_FRAMES = [
26642
26284
  "⣾",
26643
26285
  "⣽",
@@ -26648,10 +26290,40 @@ const BUSY_CHASE_FRAMES = [
26648
26290
  "⣯",
26649
26291
  "⣷"
26650
26292
  ];
26651
- /** Chase frame for a monotonic tick (the busy composer/Deep-diving marker). */
26293
+ /** Chase frame for a monotonic tick. */
26652
26294
  function busyChaseFrame(tick) {
26653
26295
  return BUSY_CHASE_FRAMES[tick % BUSY_CHASE_FRAMES.length] ?? BUSY_CHASE_FRAMES[0];
26654
26296
  }
26297
+ /** Codex shimmer timing and geometry. */
26298
+ const DEEP_DIVING_SHIMMER_DURATION_MS = 2e3;
26299
+ const DEEP_DIVING_SPARK_BREATH_DURATION_MS = 2e3;
26300
+ /**
26301
+ * Codex's 2-second shimmer sweep, expressed in terminal ticks. The sweep has
26302
+ * ten virtual columns of padding on either side and a five-column cosine
26303
+ * highlight band, so the text changes gently rather than cycling rapidly.
26304
+ */
26305
+ function deepDivingShimmerIntensity(index, tick, graphemeCount) {
26306
+ if (graphemeCount <= 0) return 0;
26307
+ const period = graphemeCount + 20;
26308
+ const position = (tick * 33 % DEEP_DIVING_SHIMMER_DURATION_MS + DEEP_DIVING_SHIMMER_DURATION_MS) % DEEP_DIVING_SHIMMER_DURATION_MS / DEEP_DIVING_SHIMMER_DURATION_MS * period;
26309
+ const distance = Math.abs(index + 10 - position);
26310
+ if (distance > 5) return 0;
26311
+ const angle = Math.PI * distance / 5;
26312
+ return .5 * (1 + Math.cos(angle));
26313
+ }
26314
+ /** Blue RGB color for one grapheme in the Codex-style shimmer. */
26315
+ function deepDivingGradientColor(index, tick, graphemeCount, base, highlight) {
26316
+ return blendRgb(highlight, base, deepDivingShimmerIntensity(index, tick, graphemeCount));
26317
+ }
26318
+ /** Smooth breathing intensity for the always-visible Deep diving sparkle. */
26319
+ function deepDivingSparkIntensity(tick) {
26320
+ const phase = (tick * 33 % DEEP_DIVING_SPARK_BREATH_DURATION_MS + DEEP_DIVING_SPARK_BREATH_DURATION_MS) % DEEP_DIVING_SPARK_BREATH_DURATION_MS / DEEP_DIVING_SPARK_BREATH_DURATION_MS;
26321
+ return .2 + .8 * (.5 + .5 * Math.cos(Math.PI * 2 * phase));
26322
+ }
26323
+ /** Blue RGB color for the breathing Deep diving sparkle. */
26324
+ function deepDivingSparkColor(tick, base, highlight) {
26325
+ return blendRgb(highlight, base, deepDivingSparkIntensity(tick));
26326
+ }
26655
26327
  /** Caret visibility: half the ticks on, half off (530ms blink). */
26656
26328
  function caretVisible(tick) {
26657
26329
  return tick % 2 === 0;
@@ -27041,6 +26713,87 @@ function effortAboveHigh(effort) {
27041
26713
  return rank !== void 0 && rank > 3;
27042
26714
  }
27043
26715
  //#endregion
26716
+ //#region src/mentions.ts
26717
+ /** Menu cap on file rows; the service owns ranking and default rows. */
26718
+ const MAX_FILE_ROWS = 20;
26719
+ /** Whether a mention token is already navigating a filesystem path. */
26720
+ function isPathLikeMentionQuery(query) {
26721
+ return /[\\/]/u.test(query);
26722
+ }
26723
+ /**
26724
+ * Create the mention API for one agent's workspace. A missing
26725
+ * `fileReferences` service (with an agent present) or `sessionReferenceResolver`
26726
+ * degrades that half to empty rows; `prepare` passes text through untouched
26727
+ * without references. An undefined agent (a bare launch before any session
26728
+ * exists) runs the official WorkspaceFileSearch over the launch cwd — the
26729
+ * same class the mounted service uses per agent — so `@` file completion
26730
+ * works from the first keystroke; session references wait for the session.
26731
+ *
26732
+ * `candidates` never reaches for `this` — the runner hands it to the input
26733
+ * editor as a detached callback, and a `this`-bound method would throw on
26734
+ * every `@` key.
26735
+ * @param ctx - context carrying the optional `fileReferences` and
26736
+ * `sessionReferenceResolver` services.
26737
+ * @param agent - the session owner; excluded from its own session candidates.
26738
+ * @param cwd - launch working directory; bounds the pre-session search.
26739
+ */
26740
+ function createMentions(ctx, agent, cwd) {
26741
+ const resolver = ctx.get("sessionReferenceResolver");
26742
+ const fileReferences = ctx.get("fileReferences");
26743
+ const sessionCapable = agent !== void 0 && resolver !== void 0;
26744
+ let preSessionSearch;
26745
+ const preSessionFiles = (query, signal) => {
26746
+ preSessionSearch ??= new WorkspaceFileSearch(cwd, {
26747
+ maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
26748
+ maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
26749
+ excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
26750
+ });
26751
+ return preSessionSearch.list(query, signal ?? new AbortController().signal);
26752
+ };
26753
+ return {
26754
+ async candidates(query, signal) {
26755
+ const needle = query.trim();
26756
+ const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch(() => []) : agent === void 0 ? preSessionFiles(needle, signal).catch(() => []) : Promise.resolve([]), sessionCapable && needle !== "" && !isPathLikeMentionQuery(needle) && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
26757
+ const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
26758
+ label: candidate.path,
26759
+ description: candidate.kind === "directory" ? "Folder" : "File",
26760
+ kind: candidate.kind,
26761
+ ...candidate.kind === "file" ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) } : {}
26762
+ }));
26763
+ const sessionRows = sessions.map((candidate) => ({
26764
+ label: formatSessionReferenceMention(candidate),
26765
+ description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
26766
+ kind: "session"
26767
+ }));
26768
+ return [...fileRows, ...sessionRows];
26769
+ },
26770
+ parse(text) {
26771
+ return parseSessionReferenceText(text);
26772
+ },
26773
+ async prepare(parsed, signal) {
26774
+ if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
26775
+ text: parsed.text,
26776
+ references: parsed.references
26777
+ };
26778
+ const prepared = await resolver.prepare(agent, [{
26779
+ type: "text",
26780
+ text: parsed.text
26781
+ }], parsed.references, signal);
26782
+ return {
26783
+ text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
26784
+ references: parsed.references,
26785
+ additionalContext: prepared.additionalContext
26786
+ };
26787
+ },
26788
+ sessionMention(candidate) {
26789
+ return formatSessionReferenceMention({
26790
+ sessionId: candidate.sessionId,
26791
+ label: candidate.label
26792
+ });
26793
+ }
26794
+ };
26795
+ }
26796
+ //#endregion
27044
26797
  //#region src/session-directory.ts
27045
26798
  /** Lightweight session-directory projection for the /resume picker. */
27046
26799
  /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
@@ -27188,6 +26941,59 @@ function collectDeletionSubtree(records, id) {
27188
26941
  return [...doomed];
27189
26942
  }
27190
26943
  /**
26944
+ * Plan one session-subtree deletion with NO filesystem side effects: collect
26945
+ * the doomed lineage, refuse when the root or ANY member is live (a live
26946
+ * child would outlive its deleted parent) or missing from the listing, and
26947
+ * order the result children-first so the executor can never leave a deleted
26948
+ * parent behind surviving children. Artifact-location guards stay at the
26949
+ * call site; this is the pure preflight they complete.
26950
+ * @param records - the full directory listing.
26951
+ * @param id - the root session id to delete.
26952
+ * @returns the ordered plan, or a user-facing refusal reason.
26953
+ */
26954
+ function planSessionDeletion(records, id) {
26955
+ const target = records.find((record) => record.header.id === id);
26956
+ if (target === void 0) return {
26957
+ ok: false,
26958
+ reason: `no persisted session matches "${id}"`
26959
+ };
26960
+ if (target.live) return {
26961
+ ok: false,
26962
+ reason: "cannot delete a live session — it is open in this or another process"
26963
+ };
26964
+ const doomed = collectDeletionSubtree(records, id);
26965
+ const byId = new Map(records.map((record) => [record.header.id, record]));
26966
+ const parentOf = /* @__PURE__ */ new Map();
26967
+ for (const record of records) parentOf.set(record.header.id, record.header.parentSession);
26968
+ const nodes = [];
26969
+ for (const candidate of doomed) {
26970
+ const record = byId.get(candidate);
26971
+ if (record === void 0) return {
26972
+ ok: false,
26973
+ reason: `no persisted session matches "${candidate}"`
26974
+ };
26975
+ if (record.live) return {
26976
+ ok: false,
26977
+ reason: `cannot delete: child session ${candidate.slice(-12)} is live — close it (and any process using it) first`
26978
+ };
26979
+ let depth = 0;
26980
+ let ancestor = parentOf.get(candidate);
26981
+ while (ancestor !== void 0 && depth < 64) {
26982
+ depth += 1;
26983
+ ancestor = parentOf.get(ancestor);
26984
+ }
26985
+ nodes.push({
26986
+ id: candidate,
26987
+ depth
26988
+ });
26989
+ }
26990
+ nodes.sort((left, right) => right.depth - left.depth);
26991
+ return {
26992
+ ok: true,
26993
+ nodes
26994
+ };
26995
+ }
26996
+ /**
27191
26997
  * Codex-style relative time for session rows ("now", "5m ago", "3h ago",
27192
26998
  * "2d ago"; older than a week falls back to the local date).
27193
26999
  * @param timestamp - epoch milliseconds of the last activity.
@@ -27970,19 +27776,26 @@ function toolDetailLines(detail, columns) {
27970
27776
  default: return detail;
27971
27777
  }
27972
27778
  }
27779
+ /** Default compact tool-card window used while the Ctrl+R fold is closed. */
27780
+ const DEFAULT_TOOL_ROWS = 3;
27781
+ /** Keep the invocation visible while making hidden tool output discoverable. */
27782
+ function compactToolLines(lines, columns) {
27783
+ if (lines.length <= DEFAULT_TOOL_ROWS) return lines;
27784
+ return [...lines.slice(0, 2), ...textLines(" … output hidden · Ctrl/Alt+R", columns, "dim").slice(0, 1)];
27785
+ }
27973
27786
  /**
27974
27787
  * Convert one durable transcript entry to its complete scrollable row model.
27975
27788
  * The source entry stays intact; only the caller's visible slice is rendered.
27976
27789
  * Wrapped continuations keep a hanging indent aligned under each row's
27977
27790
  * content (Codex history-cell alignment) instead of resetting to column 0.
27978
27791
  */
27979
- function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true) {
27792
+ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true, showToolDetails = showReasoning) {
27980
27793
  const width = Math.max(1, Math.floor(columns));
27981
27794
  switch (entry.kind) {
27982
27795
  case "user": return entry.notice ? hangingStyledLines([lineSegment(promptDisplayText(entry), "dim")], width, "⤷ ", "dim", " ", "dim") : hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
27983
27796
  case "pending": return hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
27984
27797
  case "assistant": {
27985
- const reasoning = entry.reasoning === "" ? [] : showReasoning ? reasoningLines(entry.reasoning, width) : textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ", Ctrl+R to expand" : ""})`, width, "dim");
27798
+ const reasoning = entry.reasoning === "" ? [] : showReasoning ? reasoningLines(entry.reasoning, width) : textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ", Ctrl/Alt+R to expand" : ""})`, width, "dim");
27986
27799
  const body = markdownLines(entry.text, Math.max(10, width - 2)).map((line) => ({ segments: [{
27987
27800
  text: " ",
27988
27801
  style: "plain"
@@ -27998,7 +27811,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
27998
27811
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27999
27812
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
28000
27813
  const summaryStyle = entry.state === "error" ? "error" : "dim";
28001
- return [
27814
+ const lines = [
28002
27815
  ...hangingStyledLines([
28003
27816
  lineSegment(`[${entry.ordinal}] `, "dim"),
28004
27817
  lineSegment(entry.name, "brand"),
@@ -28008,6 +27821,7 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28008
27821
  ...entry.summary === "" ? [] : hangingTextLines(entry.state === "error" ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary, width, " ⎿ ", summaryStyle, " ", summaryStyle),
28009
27822
  ...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
28010
27823
  ];
27824
+ return showToolDetails ? lines : compactToolLines(lines, width);
28011
27825
  }
28012
27826
  case "command": {
28013
27827
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
@@ -28025,7 +27839,45 @@ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningTog
28025
27839
  }
28026
27840
  /** Settled-history variant carrying the Ctrl+R reasoning fold. */
28027
27841
  function settledEntryLines(entry, columns, showReasoning) {
28028
- return transcriptEntryLines(entry, columns, showReasoning, false);
27842
+ return transcriptEntryLines(entry, columns, showReasoning, false, showReasoning);
27843
+ }
27844
+ /**
27845
+ * Clamp the live-region allocation so the flexible dynamic rows never exceed
27846
+ * the post-chrome budget. By construction the caller derives these rows from
27847
+ * the same budget; this is the runtime tripwire for a future edit that breaks
27848
+ * that derivation. Reduction order: answer first (the freshest content is the
27849
+ * live tail), then reasoning, then settled live rows; nothing goes negative.
27850
+ * @param allocation - the intended row allocation.
27851
+ * @param dynamicRows - the post-chrome row budget.
27852
+ * @returns the clamped allocation and a warning string when clamping fired.
27853
+ */
27854
+ function clampLiveAllocation(allocation, dynamicRows) {
27855
+ const live = Math.max(0, Math.floor(allocation.live));
27856
+ const reasoning = Math.max(0, Math.floor(allocation.reasoning));
27857
+ const answer = Math.max(0, Math.floor(allocation.answer));
27858
+ const budget = Math.max(0, Math.floor(dynamicRows));
27859
+ let excess = live + reasoning + answer - budget;
27860
+ if (excess <= 0) return { allocation: {
27861
+ live,
27862
+ reasoning,
27863
+ answer
27864
+ } };
27865
+ const take = (from) => {
27866
+ const cut = Math.min(from, excess);
27867
+ excess -= cut;
27868
+ return from - cut;
27869
+ };
27870
+ const clampedAnswer = take(answer);
27871
+ const clampedReasoning = excess > 0 ? take(reasoning) : reasoning;
27872
+ const clampedLive = excess > 0 ? take(live) : live;
27873
+ return {
27874
+ allocation: {
27875
+ live: clampedLive,
27876
+ reasoning: clampedReasoning,
27877
+ answer: clampedAnswer
27878
+ },
27879
+ warning: `live rows ${live} + ${reasoning} + ${answer} exceed the dynamic budget ${budget}; clamped to ${clampedLive}/${clampedReasoning}/${clampedAnswer}`
27880
+ };
28029
27881
  }
28030
27882
  //#endregion
28031
27883
  //#region src/render/editor.ts
@@ -28039,9 +27891,8 @@ function settledEntryLines(entry, columns, showReasoning) {
28039
27891
  * grapheme boundaries) so the React state stays two primitives
28040
27892
  * (value, cursor) and every operation here stays pure and testable.
28041
27893
  *
28042
- * Word motion deviates from Codex's UAX#29 segmentation in one deliberate
28043
- * way: a run of same-class characters is ONE piece, so a CJK run moves as a
28044
- * single word (two hanzi are one Alt+B step, not two).
27894
+ * Word motion follows Codex's piece semantics: whitespace separates runs,
27895
+ * punctuation runs stay atomic, and each Han grapheme is its own boundary.
28045
27896
  *
28046
27897
  * @module @deepseek-ai/dsh-code/render/editor
28047
27898
  */
@@ -28194,6 +28045,28 @@ function editorModel(value, columns) {
28194
28045
  length: value.length
28195
28046
  };
28196
28047
  }
28048
+ /** Split one row around the authoritative caret; every other row stays whole. */
28049
+ function editorRowParts(row, rowIndex, caretRow, cursor, caretEnabled = true) {
28050
+ if (!caretEnabled || rowIndex !== caretRow) return {
28051
+ before: "",
28052
+ caret: "",
28053
+ after: row.text,
28054
+ hasCaret: false
28055
+ };
28056
+ const at = row.offsets.indexOf(cursor);
28057
+ if (at < 0) return {
28058
+ before: "",
28059
+ caret: "",
28060
+ after: row.text,
28061
+ hasCaret: false
28062
+ };
28063
+ return {
28064
+ before: at > 0 ? row.text.slice(0, row.cuts[at]) : "",
28065
+ caret: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at], row.cuts[at + 1]) : " ",
28066
+ after: at < row.cuts.length - 1 ? row.text.slice(row.cuts[at + 1]) : "",
28067
+ hasCaret: true
28068
+ };
28069
+ }
28197
28070
  /** Map a cursor offset to its caret site on the wrapped rows. */
28198
28071
  function caretSite(model, offset) {
28199
28072
  const target = Math.max(0, Math.min(model.length, offset));
@@ -28218,7 +28091,9 @@ function caretSite(model, offset) {
28218
28091
  */
28219
28092
  function moveCursorVertically(model, offset, preferredColumn, delta) {
28220
28093
  const target = caretSite(model, offset).row + delta;
28221
- if (target < 0 || target >= model.rows.length || delta === 0) return offset;
28094
+ if (delta === 0) return offset;
28095
+ if (target < 0) return 0;
28096
+ if (target >= model.rows.length) return model.length;
28222
28097
  const row = model.rows[target];
28223
28098
  const wanted = Math.max(0, Math.min(preferredColumn, row.columns[row.columns.length - 1]));
28224
28099
  let best = 0;
@@ -28238,24 +28113,28 @@ function lineBounds(value, offset) {
28238
28113
  }
28239
28114
  /** Codex WORD_SEPARATORS: punctuation runs are their own word pieces. */
28240
28115
  const WORD_SEPARATORS = /* @__PURE__ */ new Set("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?");
28116
+ const HAN_GRAPHEME = /^\p{Script=Han}(?:\p{Mark}|\uFE0F)*$/u;
28117
+ const UNICODE_PUNCTUATION = /^\p{P}+$/u;
28241
28118
  function classifyGrapheme(text) {
28242
28119
  if (/^\s$/u.test(text)) return "space";
28243
- return WORD_SEPARATORS.has(text) ? "punct" : "word";
28120
+ return WORD_SEPARATORS.has(text) || UNICODE_PUNCTUATION.test(text) ? "punct" : "word";
28244
28121
  }
28245
- /** Maximal same-class runs of graphemes as [start, end) spans. */
28122
+ /** Maximal same-class runs; Han graphemes deliberately stay one run each. */
28246
28123
  function pieceRuns(value) {
28247
28124
  const runs = [];
28248
28125
  let current;
28249
28126
  for (const span of splitGraphemes(value)) {
28250
28127
  const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
28251
- if (current !== void 0 && current.class === klass) {
28128
+ const atomic = klass === "word" && HAN_GRAPHEME.test(span.text);
28129
+ if (current !== void 0 && current.class === klass && !current.atomic && !atomic) {
28252
28130
  current.end = span.end;
28253
28131
  continue;
28254
28132
  }
28255
28133
  current = {
28256
28134
  start: span.start,
28257
28135
  end: span.end,
28258
- class: klass
28136
+ class: klass,
28137
+ atomic
28259
28138
  };
28260
28139
  runs.push(current);
28261
28140
  }
@@ -28266,10 +28145,9 @@ function pieceRuns(value) {
28266
28145
  * START of the trailing non-space piece (extending over separator pieces).
28267
28146
  */
28268
28147
  function moveWordLeft(value, offset) {
28269
- const cursor = Math.max(0, Math.min(value.length, offset));
28148
+ const cursor = clampCursor(value, offset);
28270
28149
  const runs = pieceRuns(value);
28271
- let index = runs.length - 1;
28272
- while (index >= 0 && runs[index].end > cursor) index -= 1;
28150
+ let index = runs.findLastIndex((run) => run.start < cursor);
28273
28151
  if (index < 0) return 0;
28274
28152
  if (runs[index].class === "space") {
28275
28153
  index -= 1;
@@ -28284,10 +28162,9 @@ function moveWordLeft(value, offset) {
28284
28162
  * the leading non-space piece (extending over separator pieces).
28285
28163
  */
28286
28164
  function moveWordRight(value, offset) {
28287
- const cursor = Math.max(0, Math.min(value.length, offset));
28165
+ const cursor = clampCursor(value, offset);
28288
28166
  const runs = pieceRuns(value);
28289
- let index = 0;
28290
- while (index < runs.length && runs[index].start < cursor) index += 1;
28167
+ let index = runs.findIndex((run) => run.end > cursor);
28291
28168
  if (index >= runs.length) return value.length;
28292
28169
  if (runs[index].class === "space") {
28293
28170
  index += 1;
@@ -28376,6 +28253,62 @@ function insertText(value, cursor, text) {
28376
28253
  killed: void 0
28377
28254
  };
28378
28255
  }
28256
+ /** Ctrl+A: current logical line start, then the previous line start on repeat. */
28257
+ function moveToLineStart(value, cursor, crossOnRepeat) {
28258
+ const site = clampCursor(value, cursor);
28259
+ const bounds = lineBounds(value, site);
28260
+ if (!crossOnRepeat || site !== bounds.start || bounds.start === 0) return bounds.start;
28261
+ return lineBounds(value, bounds.start - 1).start;
28262
+ }
28263
+ /** Ctrl+E: current logical line end, then the next line end on repeat. */
28264
+ function moveToLineEnd(value, cursor, crossOnRepeat) {
28265
+ const site = clampCursor(value, cursor);
28266
+ const bounds = lineBounds(value, site);
28267
+ if (!crossOnRepeat || site !== bounds.end || bounds.end >= value.length) return bounds.end;
28268
+ return lineBounds(value, bounds.end + 1).end;
28269
+ }
28270
+ /**
28271
+ * Remap a captured range when all intervening edits are wholly before or
28272
+ * wholly after it. An edit overlapping either boundary invalidates the
28273
+ * anchor instead of guessing and inserting content at a surprising place.
28274
+ */
28275
+ function remapStableRange(original, current, range) {
28276
+ const start = Math.max(0, Math.min(original.length, range.start));
28277
+ const end = Math.max(start, Math.min(original.length, range.end));
28278
+ if (original === current) return {
28279
+ start,
28280
+ end
28281
+ };
28282
+ let prefix = 0;
28283
+ const shared = Math.min(original.length, current.length);
28284
+ while (prefix < shared && original[prefix] === current[prefix]) prefix += 1;
28285
+ let suffix = 0;
28286
+ while (suffix < original.length - prefix && suffix < current.length - prefix && original[original.length - 1 - suffix] === current[current.length - 1 - suffix]) suffix += 1;
28287
+ const oldChangedEnd = original.length - suffix;
28288
+ if (prefix >= end) return {
28289
+ start,
28290
+ end
28291
+ };
28292
+ if (oldChangedEnd <= start) {
28293
+ const delta = current.length - original.length;
28294
+ return {
28295
+ start: start + delta,
28296
+ end: end + delta
28297
+ };
28298
+ }
28299
+ }
28300
+ /** Replace a current range while preserving a cursor moved after capture. */
28301
+ function replaceRangePreservingCursor(value, cursor, range, replacement) {
28302
+ const start = Math.max(0, Math.min(value.length, range.start));
28303
+ const end = Math.max(start, Math.min(value.length, range.end));
28304
+ const site = clampCursor(value, cursor);
28305
+ const nextCursor = site <= start ? site : site >= end ? site + replacement.length - (end - start) : start + replacement.length;
28306
+ return {
28307
+ value: value.slice(0, start) + replacement + value.slice(end),
28308
+ cursor: nextCursor,
28309
+ killed: void 0
28310
+ };
28311
+ }
28379
28312
  /**
28380
28313
  * Composer editor row budget: the editor itself never grows past this many
28381
28314
  * physical rows; deeper drafts scroll internally to keep the caret visible.
@@ -28385,14 +28318,73 @@ function composerMaxRows(terminalRows) {
28385
28318
  return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
28386
28319
  }
28387
28320
  /**
28388
- * Codex `should_handle_navigation`: Up/Down walk history only from an empty
28389
- * draft, or from a boundary of a draft that still exactly matches the last
28390
- * recalled entry. Any interior cursor position keeps vertical caret movement.
28321
+ * History navigation starts with Up on an empty draft, or after visual
28322
+ * movement has reached the directional text edge of an unchanged recalled
28323
+ * entry. Every other position remains under textarea movement.
28324
+ */
28325
+ function shouldRecallNavigate(value, cursor, lastRecalled, direction) {
28326
+ if (value === "") return direction < 0;
28327
+ if (lastRecalled !== value) return false;
28328
+ return direction < 0 ? cursor === 0 : cursor === value.length;
28329
+ }
28330
+ //#endregion
28331
+ //#region src/keyboard.ts
28332
+ /**
28333
+ * Keyboard enhancement protocol (Codex `keyboard_modes` parity) and the
28334
+ * kitty CSI-u normalization layer.
28335
+ *
28336
+ * The TUI pushes the kitty keyboard protocol with DISAMBIGUATE_ESCAPE_CODES
28337
+ * and REPORT_ALTERNATE_KEYS (flags 1|4 = `\x1b[>5u`). Event types are
28338
+ * deliberately NOT requested: Ink 5's parser cannot decode the
28339
+ * `:event-type` suffix, and repeat/release reporting buys this surface
28340
+ * nothing.
28341
+ *
28342
+ * Ink 5 also cannot parse most CSI-u forms at all — they fall through its
28343
+ * regex as unnamed sequences and get INSERTED AS DRAFT TEXT. The composer's
28344
+ * stdin read patch therefore rewrites every CSI-u form it can decode back
28345
+ * to the legacy byte or canonical sequence the existing key handling
28346
+ * already understands, before Ink ever parses the chunk.
28347
+ * @module @deepseek-ai/dsh-code/keyboard
28348
+ */
28349
+ /** Push keyboard enhancement (modifyOtherKeys off, kitty flags 1|4). */
28350
+ const KEYBOARD_ENHANCE_ENABLE = "\x1B[>4;0m\x1B[>5u";
28351
+ /** Pop the enhancement stack and reset modifyOtherKeys (exit path). */
28352
+ const KEYBOARD_ENHANCE_DISABLE = "\x1B[<u\x1B[>4;0m";
28353
+ const DSH_ENABLE_KEYBOARD_ENHANCEMENT = "DSH_ENABLE_KEYBOARD_ENHANCEMENT";
28354
+ function parseBooleanEnv(value) {
28355
+ if (value === void 0) return void 0;
28356
+ const normalized = value.trim().toLowerCase();
28357
+ if (normalized === "1" || normalized === "true" || normalized === "yes") return true;
28358
+ if (normalized === "0" || normalized === "false" || normalized === "no") return false;
28359
+ }
28360
+ /** True when the process is running inside the VS Code integrated terminal. */
28361
+ function isVsCodeTerminalEnv(env = process.env) {
28362
+ return env.TERM_PROGRAM?.trim().toLowerCase() === "vscode" || env.VSCODE_INJECTION === "1";
28363
+ }
28364
+ /** Whether to push Kitty keyboard enhancement for the current terminal. */
28365
+ function shouldEnableKeyboardEnhancement(env = process.env) {
28366
+ if (parseBooleanEnv(env["DSH_DISABLE_KEYBOARD_ENHANCEMENT"]) === true) return false;
28367
+ const explicitEnable = parseBooleanEnv(env[DSH_ENABLE_KEYBOARD_ENHANCEMENT]);
28368
+ if (explicitEnable !== void 0) return explicitEnable;
28369
+ return !isVsCodeTerminalEnv(env);
28370
+ }
28371
+ /** Enable bracketed paste reporting. */
28372
+ const BRACKETED_PASTE_ENABLE = "\x1B[?2004h";
28373
+ /** Disable bracketed paste reporting. */
28374
+ const BRACKETED_PASTE_DISABLE = "\x1B[?2004l";
28375
+ /** Enable terminal focus-in/focus-out reporting (xterm focus protocol). */
28376
+ const TERMINAL_FOCUS_REPORT_ENABLE = "\x1B[?1004h";
28377
+ /** Disable terminal focus-in/focus-out reporting. */
28378
+ const TERMINAL_FOCUS_REPORT_DISABLE = "\x1B[?1004l";
28379
+ /**
28380
+ * Remove xterm focus reports from one input chunk and update the caller's
28381
+ * focus state. Focus reports are terminal protocol, not composer text.
28391
28382
  */
28392
- function shouldRecallNavigate(value, cursor, lastRecalled) {
28393
- if (value === "") return true;
28394
- if (cursor !== 0 && cursor !== value.length) return false;
28395
- return lastRecalled === value;
28383
+ function stripTerminalFocusEvents(chunk, onFocus) {
28384
+ return chunk.replace(/\x1b\[(I|O)/gu, (_whole, event) => {
28385
+ onFocus(event === "I");
28386
+ return "";
28387
+ });
28396
28388
  }
28397
28389
  /** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
28398
28390
  const PASTE_START_MARKER = "[200~";
@@ -28464,6 +28456,98 @@ function normalizeKeyboardChunk(chunk) {
28464
28456
  }) ?? whole;
28465
28457
  });
28466
28458
  }
28459
+ const HOME_SEQUENCES = [
28460
+ "\x1B[H",
28461
+ "\x1B[1~",
28462
+ "\x1B[7~",
28463
+ "\x1BOH"
28464
+ ];
28465
+ const END_SEQUENCES = [
28466
+ "\x1B[F",
28467
+ "\x1B[4~",
28468
+ "\x1B[8~",
28469
+ "\x1BOF"
28470
+ ];
28471
+ /** Parse one CSI functional-key sequence at `offset`. */
28472
+ function functionalToken(chunk, offset) {
28473
+ const tail = chunk.slice(offset);
28474
+ for (const sequence of HOME_SEQUENCES) if (tail.startsWith(sequence)) return {
28475
+ token: { kind: "home" },
28476
+ length: sequence.length
28477
+ };
28478
+ for (const sequence of END_SEQUENCES) if (tail.startsWith(sequence)) return {
28479
+ token: { kind: "end" },
28480
+ length: sequence.length
28481
+ };
28482
+ const modifiedHome = /^\x1b\[1;(\d+)H/u.exec(tail);
28483
+ if (modifiedHome !== null) return {
28484
+ token: { kind: "home" },
28485
+ length: modifiedHome[0].length
28486
+ };
28487
+ const modifiedEnd = /^\x1b\[1;(\d+)F/u.exec(tail);
28488
+ if (modifiedEnd !== null) return {
28489
+ token: { kind: "end" },
28490
+ length: modifiedEnd[0].length
28491
+ };
28492
+ const modifiedDelete = /^\x1b\[3(?:;(\d+))?~/u.exec(tail);
28493
+ if (modifiedDelete !== null) {
28494
+ const modifiers = Number.parseInt(modifiedDelete[1] ?? "1", 10) - 1;
28495
+ return {
28496
+ token: { kind: (modifiers & 2) !== 0 || (modifiers & 4) !== 0 ? "delete-word-forward" : "delete-forward" },
28497
+ length: modifiedDelete[0].length
28498
+ };
28499
+ }
28500
+ }
28501
+ /**
28502
+ * Tokenize a stdin chunk containing at least one editor-only key. Ink calls
28503
+ * `useInput` once for a pasted/batched chunk, so preserving each action here
28504
+ * prevents repeated Backspace/Home/End/Delete presses from collapsing into
28505
+ * one blurred key event. Unknown escape sequences return undefined and stay
28506
+ * under Ink's ownership.
28507
+ */
28508
+ function tokenizeRawEditorChunk(chunk) {
28509
+ const tokens = [];
28510
+ let text = "";
28511
+ let special = false;
28512
+ const flushText = () => {
28513
+ if (text === "") return;
28514
+ tokens.push({
28515
+ kind: "text",
28516
+ text
28517
+ });
28518
+ text = "";
28519
+ };
28520
+ for (let offset = 0; offset < chunk.length;) {
28521
+ if (chunk.startsWith("\x1B", offset) || chunk.startsWith("\x1B\b", offset)) {
28522
+ flushText();
28523
+ tokens.push({ kind: "delete-word-backward" });
28524
+ special = true;
28525
+ offset += 2;
28526
+ continue;
28527
+ }
28528
+ const functional = functionalToken(chunk, offset);
28529
+ if (functional !== void 0) {
28530
+ flushText();
28531
+ tokens.push(functional.token);
28532
+ special = true;
28533
+ offset += functional.length;
28534
+ continue;
28535
+ }
28536
+ const char = chunk[offset];
28537
+ if (char === "" || char === "\b") {
28538
+ flushText();
28539
+ tokens.push({ kind: "delete-backward" });
28540
+ special = true;
28541
+ offset += 1;
28542
+ continue;
28543
+ }
28544
+ if (char === "\x1B") return void 0;
28545
+ text += char;
28546
+ offset += 1;
28547
+ }
28548
+ flushText();
28549
+ return special ? tokens : void 0;
28550
+ }
28467
28551
  //#endregion
28468
28552
  //#region src/kernel-panels.ts
28469
28553
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
@@ -29923,11 +30007,15 @@ async function inspectImagePaths(paths, attachments, cwd = process.cwd()) {
29923
30007
  return inspected;
29924
30008
  }
29925
30009
  /** Read, validate, and persist an ordered image path list as model content blocks. */
29926
- async function saveImagePaths(paths, attachments) {
30010
+ async function saveImagePaths(paths, attachments, signal) {
29927
30011
  if (paths.length === 0) return [];
29928
30012
  if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
30013
+ const checkCancelled = () => {
30014
+ if (signal?.aborted === true) throw new Error("image submission cancelled");
30015
+ };
29929
30016
  const inputs = [];
29930
30017
  for (const path of paths) {
30018
+ checkCancelled();
29931
30019
  let data;
29932
30020
  try {
29933
30021
  data = await readFile(path);
@@ -29942,7 +30030,10 @@ async function saveImagePaths(paths, attachments) {
29942
30030
  name: basename(path)
29943
30031
  });
29944
30032
  }
29945
- return (await attachments.saveImages(inputs)).map((attachment) => ({
30033
+ checkCancelled();
30034
+ const refs = await attachments.saveImages(inputs);
30035
+ checkCancelled();
30036
+ return refs.map((attachment) => ({
29946
30037
  type: "image",
29947
30038
  attachment
29948
30039
  }));
@@ -30058,6 +30149,10 @@ const LOCAL_COMMANDS = [
30058
30149
  label: "/subagent",
30059
30150
  description: "choose the model delegated subagents run on"
30060
30151
  },
30152
+ {
30153
+ label: "/vscode-keys",
30154
+ description: "pass ctrl+r through the vs code terminal"
30155
+ },
30061
30156
  {
30062
30157
  label: "/delete",
30063
30158
  description: "delete a session and its subagent threads"
@@ -30098,14 +30193,15 @@ function padColumns(text, width) {
30098
30193
  return clipped + " ".repeat(Math.max(0, width - visibleColumns(clipped)));
30099
30194
  }
30100
30195
  /** Interval-driven frame counter for one self-contained animated leaf. */
30101
- function useFrames(intervalMs) {
30196
+ function useFrames(intervalMs, active = true) {
30102
30197
  const [tick, setTick] = (0, import_react.useState)(0);
30103
30198
  (0, import_react.useEffect)(() => {
30199
+ if (!active) return;
30104
30200
  const id = setInterval(() => setTick((current) => current + 1), intervalMs);
30105
30201
  return () => {
30106
30202
  clearInterval(id);
30107
30203
  };
30108
- }, [intervalMs]);
30204
+ }, [active, intervalMs]);
30109
30205
  return tick;
30110
30206
  }
30111
30207
  /**
@@ -30122,12 +30218,7 @@ function useStableInput(handler, active) {
30122
30218
  }, []);
30123
30219
  useInput(stableHandler, { isActive: active });
30124
30220
  }
30125
- /**
30126
- * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
30127
- * ring trail clockwise around the eight outer positions (8 frames × 125ms =
30128
- * the web's 1s cycle). Replaces the plain busy ellipsis as the composer's
30129
- * prompt marker and leads the Deep-diving line.
30130
- */
30221
+ /** The original web StateDot chase used by the busy composer marker. */
30131
30222
  function BusyChase() {
30132
30223
  const tick = useFrames(125);
30133
30224
  return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, busyChaseFrame(tick) + " ");
@@ -30137,21 +30228,55 @@ function Caret() {
30137
30228
  const tick = useFrames(530);
30138
30229
  return (0, import_react.createElement)(Text, null, caretVisible(tick) ? "▍" : " ");
30139
30230
  }
30140
- /** Blinking input cursor: inverse block while the caret phase is on. */
30141
- function CursorBlock({ char }) {
30142
- const tick = useFrames(530);
30143
- return (0, import_react.createElement)(Text, { inverse: caretVisible(tick) || void 0 }, char);
30231
+ /** One resettable input-caret phase shared by the entire composer. */
30232
+ function useCursorBlink(active) {
30233
+ const [epoch, setEpoch] = (0, import_react.useState)(0);
30234
+ const [visible, setVisible] = (0, import_react.useState)(true);
30235
+ (0, import_react.useEffect)(() => {
30236
+ setVisible(true);
30237
+ if (!active) return;
30238
+ const id = setInterval(() => setVisible((current) => !current), 530);
30239
+ return () => {
30240
+ clearInterval(id);
30241
+ };
30242
+ }, [active, epoch]);
30243
+ return {
30244
+ visible,
30245
+ reset: (0, import_react.useCallback)(() => {
30246
+ setVisible(true);
30247
+ setEpoch((current) => current + 1);
30248
+ }, [])
30249
+ };
30250
+ }
30251
+ /**
30252
+ * One bounded line painted with the deep-diving shimmer: a continuously
30253
+ * moving blue gradient across graphemes, the `✻` glyph in the breathing
30254
+ * spark color. Shared by the busy line and the collapsed thinking marker;
30255
+ * always exactly one row (truncate-end) so the live budget stays exact.
30256
+ */
30257
+ function ShimmerLine({ text }) {
30258
+ const tick = useFrames(33);
30259
+ const palette = getPalette();
30260
+ const graphemes = splitGraphemes(text);
30261
+ return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...graphemes.map((grapheme, index) => {
30262
+ const sparkle = grapheme.text === "✻";
30263
+ return (0, import_react.createElement)(Text, {
30264
+ key: `${grapheme.start}-${grapheme.end}`,
30265
+ color: inkColor(sparkle ? deepDivingSparkColor(tick, palette.brandDeep, palette.brandBright) : deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, palette.brandBright)),
30266
+ bold: sparkle || void 0
30267
+ }, grapheme.text);
30268
+ }));
30144
30269
  }
30145
30270
  /**
30146
- * The busy line, web TurnStatus contract: the StateDot chase leads the plain
30147
- * `Deep diving...` label, with the elapsed clock appended only once the turn
30148
- * has clearly been running (15s) — anchored to `turn/start` so a resumed
30149
- * mid-turn keeps the real time.
30271
+ * The busy line, web TurnStatus contract: a continuously moving blue gradient
30272
+ * paints the complete `Deep diving...` label, with the elapsed clock appended
30273
+ * only once the turn has clearly been running (15s) — anchored to `turn/start`
30274
+ * so a resumed mid-turn keeps the real time.
30150
30275
  */
30151
30276
  function DeepDivingLine({ since }) {
30152
- useFrames(1e3);
30153
30277
  const elapsed = since === 0 ? 0 : Date.now() - since;
30154
- return (0, import_react.createElement)(Box, { flexDirection: "row" }, (0, import_react.createElement)(BusyChase), (0, import_react.createElement)(Text, { dimColor: true }, elapsed >= 15e3 ? `Deep diving... ${runClock(elapsed)}` : "Deep diving..."));
30278
+ const text = elapsed >= 15e3 ? `✻ Deep diving... ${runClock(elapsed)}` : "Deep diving...";
30279
+ return (0, import_react.createElement)(ShimmerLine, { text });
30155
30280
  }
30156
30281
  /**
30157
30282
  * The streaming buffer rendered with a hard size cap: the live region must
@@ -30409,11 +30534,12 @@ function todoMark(status) {
30409
30534
  }
30410
30535
  /**
30411
30536
  * One-row live subagent summary (the Codex agent status feed, compressed to
30412
- * the transcript's budget): running count, total, and the most recently
30413
- * active child's current activity. One line, never more the full view is
30414
- * the /agents panel.
30537
+ * the transcript's budget): running count, the observed total (the row cap
30538
+ * is a display budget, not the fan-out size), and the most recently active
30539
+ * child's current activity. One line, never more — the full view is the
30540
+ * /agents panel.
30415
30541
  */
30416
- function AgentsLine({ rows }) {
30542
+ function AgentsLine({ rows, total }) {
30417
30543
  if (rows.length === 0) return void 0;
30418
30544
  const running = rows.filter((row) => row.state !== "done").length;
30419
30545
  const newest = [...rows].sort((left, right) => right.updatedAt - left.updatedAt)[0];
@@ -30422,7 +30548,7 @@ function AgentsLine({ rows }) {
30422
30548
  color: inkColor(getPalette().brand),
30423
30549
  bold: true,
30424
30550
  wrap: "truncate-end"
30425
- }, `agents ${running} live`, (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, ` · ${rows.length} total · /agents`), (0, import_react.createElement)(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`)));
30551
+ }, `agents ${running} live`, (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, ` · ${total} total · /agents`), (0, import_react.createElement)(Text, { color: inkColor(getPalette().text) }, ` · ${mark} ${newest.label} ${newest.activity}`)));
30426
30552
  }
30427
30553
  /** One-row todo summary: task count cannot grow the live Ink tree. */
30428
30554
  function TodoPanel({ todos }) {
@@ -31641,7 +31767,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31641
31767
  key: "key-inspector",
31642
31768
  dimColor: true,
31643
31769
  wrap: "truncate-end"
31644
- }, " ctrl+o history details · ctrl+r thinking · shift+tab permission preset"),
31770
+ }, " ctrl+o history details · ctrl/alt+r thinking · shift+tab permission preset"),
31645
31771
  (0, import_react.createElement)(Text, {
31646
31772
  key: "key-cancel",
31647
31773
  dimColor: true,
@@ -31730,49 +31856,6 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
31730
31856
  function verboseLine(text, columns) {
31731
31857
  return truncateColumns(displayText(text).replace(/\n/gu, " ↵ ").replace(/\t/gu, " "), Math.max(1, columns));
31732
31858
  }
31733
- /** Identify one whole-chunk key sequence Ink drops or blurs. */
31734
- function annotateRawKey(chunk) {
31735
- switch (chunk) {
31736
- case "": return "delete-backward";
31737
- case "\x1B":
31738
- case "\x1B\b": return "delete-word-backward";
31739
- case "\x1B[3~":
31740
- case "\x1B[3;2~": return "delete-forward";
31741
- case "\x1B[3;3~":
31742
- case "\x1B[3;5~": return "delete-word-forward";
31743
- case "\x1B[H":
31744
- case "\x1B[1~":
31745
- case "\x1B[7~":
31746
- case "\x1BOH": return "home";
31747
- case "\x1B[F":
31748
- case "\x1B[4~":
31749
- case "\x1B[8~":
31750
- case "\x1BOF": return "end";
31751
- default: return;
31752
- }
31753
- }
31754
- /**
31755
- * One-row editor window keeping the logical cursor visible in long drafts.
31756
- * The caret and its surroundings slice at grapheme boundaries: splitting a
31757
- * star-plane surrogate pair would render an isolated half under the block
31758
- * caret with a width the terminal never draws.
31759
- */
31760
- function editorWindow(value, cursor, columns) {
31761
- const width = Math.max(1, columns);
31762
- const normalize = (text) => displayText(text).replace(/\n/gu, "↵").replace(/\t/gu, " ");
31763
- const site = clampCursor(value, cursor);
31764
- const caretSpan = splitGraphemes(value).find((span) => span.start === site);
31765
- const caret = caretSpan === void 0 ? " " : normalize(caretSpan.text);
31766
- const rest = value.slice(caretSpan === void 0 ? site : caretSpan.end);
31767
- const remaining = Math.max(0, width - visibleColumns(caret));
31768
- const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)));
31769
- const beforeBudget = Math.max(0, remaining - afterBudget);
31770
- return {
31771
- before: beforeBudget === 0 ? "" : displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text,
31772
- caret,
31773
- after: afterBudget === 0 ? "" : truncateColumns(normalize(rest), afterBudget)
31774
- };
31775
- }
31776
31859
  /** The empty-composer placeholder text (shared by the static and wave paths). */
31777
31860
  const COMPOSER_PLACEHOLDER = "type a message · / commands · @ mentions";
31778
31861
  /** Adjacent cells with identical styling merge into one styled Text span. */
@@ -32021,9 +32104,11 @@ function CompletionMenu({ active, mention, index, rows }) {
32021
32104
  * While a modal (approval / question / model panel) owns the keys, the
32022
32105
  * box passes every key through untouched.
32023
32106
  */
32024
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
32107
+ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openJobs, openStatusline, openTheme, openHistory, openAgents, openSubagent, openTodos, openDelete, openDiff, reviewChanges, deleteConfirm, confirmDelete, cancelDelete, createSession, forkSession, cancelSessionSwitch, notify, applyEditorKeys, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, inspectImages, prepareImages, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
32025
32108
  const columns = useStdout().stdout?.columns ?? 80;
32109
+ const editorColumns = Math.max(1, columns - 6);
32026
32110
  const stdin = useStdin().stdin;
32111
+ const focusReporting = isVsCodeTerminalEnv();
32027
32112
  const [value, setValue] = (0, import_react.useState)("");
32028
32113
  const [cursor, setCursor] = (0, import_react.useState)(0);
32029
32114
  const valueRef = (0, import_react.useRef)(value);
@@ -32034,22 +32119,37 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32034
32119
  const draftImagesRef = (0, import_react.useRef)(draftImages);
32035
32120
  draftImagesRef.current = draftImages;
32036
32121
  const [preparingImages, setPreparingImages] = (0, import_react.useState)(false);
32122
+ const prepareAbortRef = (0, import_react.useRef)(void 0);
32123
+ const prepareEpochRef = (0, import_react.useRef)(0);
32124
+ const { visible: cursorVisible, reset: resetCursorBlink } = useCursorBlink(active && !frozen && !preparingImages);
32125
+ (0, import_react.useEffect)(() => () => {
32126
+ prepareEpochRef.current += 1;
32127
+ prepareAbortRef.current?.abort();
32128
+ }, []);
32037
32129
  const killRef = (0, import_react.useRef)("");
32038
32130
  const preferredColumnRef = (0, import_react.useRef)(null);
32039
32131
  const editorScrollRef = (0, import_react.useRef)(0);
32040
32132
  const pasteBracketRef = (0, import_react.useRef)(false);
32041
32133
  /** Cancels the pending lost-paste safety timer (undefined when disarmed). */
32042
32134
  const pasteBracketCancelRef = (0, import_react.useRef)(void 0);
32043
- /** Annotation of the stdin chunk Ink is about to deliver to useInput. */
32044
- const rawAnnotation = (0, import_react.useRef)(void 0);
32135
+ /** Ordered editor tokens from the stdin chunk Ink is about to deliver. */
32136
+ const rawEditorTokens = (0, import_react.useRef)(void 0);
32137
+ /** VS Code focus state from xterm focus-report events; starts focused. */
32138
+ const terminalFocusedRef = (0, import_react.useRef)(true);
32045
32139
  const recall = (0, import_react.useRef)(beginRecall([], ""));
32140
+ (0, import_react.useEffect)(() => {
32141
+ preferredColumnRef.current = null;
32142
+ }, [editorColumns]);
32046
32143
  (0, import_react.useEffect)(() => {
32047
32144
  if (historyFill === void 0) return;
32048
32145
  const safe = sanitizeDraftText(historyFill.text);
32049
32146
  draftImagesRef.current = [];
32050
32147
  setDraftImages([]);
32148
+ valueRef.current = safe;
32149
+ cursorRef.current = safe.length;
32051
32150
  setValue(safe);
32052
32151
  setCursor(safe.length);
32152
+ resetCursorBlink();
32053
32153
  preferredColumnRef.current = null;
32054
32154
  setDismissedMenuValue(void 0);
32055
32155
  recall.current = {
@@ -32062,7 +32162,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32062
32162
  }, [
32063
32163
  historyFill,
32064
32164
  recallSpace,
32065
- historyConsumed
32165
+ historyConsumed,
32166
+ resetCursorBlink
32066
32167
  ]);
32067
32168
  (0, import_react.useEffect)(() => {
32068
32169
  setDraftImages((current) => {
@@ -32078,13 +32179,16 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32078
32179
  const chunk = originalRead(...args);
32079
32180
  if (chunk === null) return chunk;
32080
32181
  const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
32081
- rawAnnotation.current = annotateRawKey(normalized);
32082
- return normalized;
32182
+ const input = focusReporting ? stripTerminalFocusEvents(normalized, (focused) => {
32183
+ terminalFocusedRef.current = focused;
32184
+ }) : normalized;
32185
+ rawEditorTokens.current = tokenizeRawEditorChunk(input);
32186
+ return input;
32083
32187
  };
32084
32188
  return () => {
32085
32189
  stdin.read = originalRead;
32086
32190
  };
32087
- }, [stdin]);
32191
+ }, [focusReporting, stdin]);
32088
32192
  if (recall.current.entries !== recallSpace) {
32089
32193
  const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
32090
32194
  recall.current = {
@@ -32106,6 +32210,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32106
32210
  };
32107
32211
  const mentionActive = mentionToken !== void 0;
32108
32212
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
32213
+ const mentionRequestRef = (0, import_react.useRef)(0);
32109
32214
  const sameImagePath = (left, right) => process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
32110
32215
  const uniqueImageMarker = (name, source, reserved = []) => {
32111
32216
  const safeName = singleLineText(sanitizeDraftText(name));
@@ -32131,6 +32236,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32131
32236
  return true;
32132
32237
  };
32133
32238
  const insertDroppedImages = (paths) => {
32239
+ const originalValue = valueRef.current;
32240
+ const originalCursor = cursorRef.current;
32134
32241
  notify(`checking ${paths.length} image${paths.length === 1 ? "" : "s"}…`);
32135
32242
  inspectImages(paths).then((inspected) => {
32136
32243
  const additions = [];
@@ -32148,14 +32255,24 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32148
32255
  notify("those images are already attached", "warning");
32149
32256
  return;
32150
32257
  }
32151
- const at = cursorRef.current;
32152
32258
  const current = valueRef.current;
32259
+ const anchor = remapStableRange(originalValue, current, {
32260
+ start: originalCursor,
32261
+ end: originalCursor
32262
+ });
32263
+ if (anchor === void 0) {
32264
+ notify("draft changed at the image drop point; drop the images again", "warning");
32265
+ return;
32266
+ }
32267
+ const at = anchor.start;
32153
32268
  const insertion = `${at > 0 && !/\s$/u.test(current.slice(0, at)) ? " " : ""}${markers.join(" ")}${current.slice(at) === "" ? "" : " "}`;
32154
- const next = current.slice(0, at) + insertion + current.slice(at);
32155
- valueRef.current = next;
32156
- cursorRef.current = at + insertion.length;
32157
- setValue(next);
32158
- setCursor(cursorRef.current);
32269
+ const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, insertion);
32270
+ const nextCursor = current === originalValue && cursorRef.current === originalCursor ? at + insertion.length : edit.cursor;
32271
+ valueRef.current = edit.value;
32272
+ cursorRef.current = nextCursor;
32273
+ setValue(edit.value);
32274
+ setCursor(nextCursor);
32275
+ resetCursorBlink();
32159
32276
  const nextImages = [...draftImagesRef.current, ...additions];
32160
32277
  draftImagesRef.current = nextImages;
32161
32278
  setDraftImages(nextImages);
@@ -32165,14 +32282,21 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32165
32282
  });
32166
32283
  };
32167
32284
  (0, import_react.useEffect)(() => {
32285
+ const requestId = mentionRequestRef.current + 1;
32286
+ mentionRequestRef.current = requestId;
32168
32287
  if (!active || !mentionActive) {
32169
32288
  setMentionRows([]);
32170
32289
  return;
32171
32290
  }
32172
32291
  const controller = new AbortController();
32173
- setMentionRows([]);
32174
- loadMentions(mentionToken.query, controller.signal).then((rows) => setMentionRows(rows), () => {});
32292
+ const query = mentionToken.query;
32293
+ const timer = setTimeout(() => {
32294
+ loadMentions(query, controller.signal).then((rows) => {
32295
+ if (!controller.signal.aborted && mentionRequestRef.current === requestId) setMentionRows(rows);
32296
+ }, () => {});
32297
+ }, 50);
32175
32298
  return () => {
32299
+ clearTimeout(timer);
32176
32300
  controller.abort();
32177
32301
  };
32178
32302
  }, [
@@ -32180,8 +32304,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32180
32304
  mentionActive,
32181
32305
  mentionToken?.query
32182
32306
  ]);
32183
- const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value;
32184
- const menuRows = mentionActive ? mentionRows.map((row) => ({
32307
+ const menuActive = !preparingImages && (slashActive || mentionActive) && dismissedMenuValue !== value;
32308
+ const visibleMentionRows = mentionToken !== void 0 && isPathLikeMentionQuery(mentionToken.query) ? mentionRows.filter((row) => row.kind !== "session") : mentionRows;
32309
+ const menuRows = mentionActive ? visibleMentionRows.map((row) => ({
32185
32310
  label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
32186
32311
  description: row.description,
32187
32312
  origin: "mention"
@@ -32189,34 +32314,45 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32189
32314
  /** Accept the highlighted completion-menu candidate into the draft. */
32190
32315
  const acceptMenuCandidate = () => {
32191
32316
  if (mentionActive && mentionToken !== void 0) {
32192
- const row = mentionRows[completionIndex % mentionRows.length];
32317
+ if (visibleMentionRows.length === 0) return;
32318
+ const row = visibleMentionRows[completionIndex % visibleMentionRows.length];
32193
32319
  if (row !== void 0) {
32194
32320
  if (row.kind === "file" && row.path !== void 0 && looksLikeImagePath(row.path)) {
32195
32321
  const tokenText = value.slice(mentionToken.start, cursor);
32196
32322
  const start = mentionToken.start;
32323
+ const originalValue = value;
32197
32324
  notify(`checking image ${basename(row.path)}…`);
32198
32325
  inspectImages([row.path]).then((inspected) => {
32199
32326
  const inspection = inspected[0];
32200
32327
  if (inspection === void 0) return;
32201
32328
  const current = valueRef.current;
32202
- if (current.slice(start, start + tokenText.length) !== tokenText) return;
32329
+ const anchor = remapStableRange(originalValue, current, {
32330
+ start,
32331
+ end: start + tokenText.length
32332
+ });
32333
+ if (anchor === void 0 || current.slice(anchor.start, anchor.end) !== tokenText) {
32334
+ notify("draft changed around the image mention; select it again", "warning");
32335
+ return;
32336
+ }
32203
32337
  if (draftImagesRef.current.some((image) => sameImagePath(image.path, inspection.path))) {
32204
- const next = current.slice(0, start) + current.slice(start + tokenText.length);
32205
- valueRef.current = next;
32206
- cursorRef.current = start;
32207
- setValue(next);
32208
- setCursor(start);
32209
- setDismissedMenuValue(next);
32338
+ const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, "");
32339
+ valueRef.current = edit.value;
32340
+ cursorRef.current = edit.cursor;
32341
+ setValue(edit.value);
32342
+ setCursor(edit.cursor);
32343
+ resetCursorBlink();
32344
+ setDismissedMenuValue(edit.value);
32210
32345
  notify(`${inspection.name} is already attached`, "warning");
32211
32346
  return;
32212
32347
  }
32213
32348
  const marker = uniqueImageMarker(inspection.name, "mention");
32214
- const next = current.slice(0, start) + marker + current.slice(start + tokenText.length);
32215
- valueRef.current = next;
32216
- cursorRef.current = start + marker.length;
32217
- setValue(next);
32218
- setCursor(cursorRef.current);
32219
- setDismissedMenuValue(next);
32349
+ const edit = replaceRangePreservingCursor(current, cursorRef.current, anchor, marker);
32350
+ valueRef.current = edit.value;
32351
+ cursorRef.current = edit.cursor;
32352
+ setValue(edit.value);
32353
+ setCursor(edit.cursor);
32354
+ resetCursorBlink();
32355
+ setDismissedMenuValue(edit.value);
32220
32356
  registerDraftImage(inspection, marker);
32221
32357
  notify(`${inspection.name} ready for the next message`);
32222
32358
  }, (reason) => {
@@ -32227,14 +32363,25 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32227
32363
  return;
32228
32364
  }
32229
32365
  const insertion = row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`;
32230
- setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
32231
- setCursor(mentionToken.start + insertion.length);
32366
+ const nextValue = value.slice(0, mentionToken.start) + insertion + value.slice(cursor);
32367
+ const nextCursor = mentionToken.start + insertion.length;
32368
+ valueRef.current = nextValue;
32369
+ cursorRef.current = nextCursor;
32370
+ setValue(nextValue);
32371
+ setCursor(nextCursor);
32372
+ resetCursorBlink();
32232
32373
  }
32233
32374
  } else {
32375
+ if (candidates.length === 0) return;
32234
32376
  const candidate = candidates[completionIndex % candidates.length];
32235
32377
  if (candidate !== void 0) {
32236
- setValue(`${candidate.label} `);
32237
- setCursor(candidate.label.length + 1);
32378
+ const nextValue = `${candidate.label} `;
32379
+ const nextCursor = candidate.label.length + 1;
32380
+ valueRef.current = nextValue;
32381
+ cursorRef.current = nextCursor;
32382
+ setValue(nextValue);
32383
+ setCursor(nextCursor);
32384
+ resetCursorBlink();
32238
32385
  }
32239
32386
  }
32240
32387
  setCompletionIndex(0);
@@ -32243,21 +32390,101 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32243
32390
  /** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
32244
32391
  const applyEdit = (edit) => {
32245
32392
  if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
32393
+ valueRef.current = edit.value;
32394
+ cursorRef.current = edit.cursor;
32246
32395
  setValue(edit.value);
32247
32396
  setCursor(edit.cursor);
32397
+ resetCursorBlink();
32248
32398
  preferredColumnRef.current = null;
32249
32399
  setCompletionIndex(0);
32250
32400
  setDismissedMenuValue(void 0);
32251
32401
  };
32252
32402
  /** Move the cursor without editing; horizontal moves clear the column preference. */
32253
32403
  const moveCursorTo = (next) => {
32254
- if (next === cursor) return;
32404
+ resetCursorBlink();
32405
+ if (next === cursorRef.current) return;
32406
+ cursorRef.current = next;
32255
32407
  setCursor(next);
32256
32408
  preferredColumnRef.current = null;
32257
32409
  };
32258
- useInput((input, key) => {
32410
+ /** Apply an ordered raw-key batch against one current draft snapshot. */
32411
+ const applyRawEditorTokens = (tokens) => {
32412
+ let nextValue = valueRef.current;
32413
+ let nextCursor = cursorRef.current;
32414
+ for (const token of tokens) {
32415
+ if (token.kind === "text") {
32416
+ const edit = insertText(nextValue, nextCursor, token.text);
32417
+ nextValue = edit.value;
32418
+ nextCursor = edit.cursor;
32419
+ continue;
32420
+ }
32421
+ if (token.kind === "home") {
32422
+ nextCursor = moveToLineStart(nextValue, nextCursor, false);
32423
+ continue;
32424
+ }
32425
+ if (token.kind === "end") {
32426
+ nextCursor = moveToLineEnd(nextValue, nextCursor, false);
32427
+ continue;
32428
+ }
32429
+ const edit = token.kind === "delete-backward" ? deleteBackward(nextValue, nextCursor) : token.kind === "delete-word-backward" ? deleteWordBackward(nextValue, nextCursor) : token.kind === "delete-forward" ? deleteForward(nextValue, nextCursor) : deleteWordForward(nextValue, nextCursor);
32430
+ if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
32431
+ nextValue = edit.value;
32432
+ nextCursor = edit.cursor;
32433
+ }
32434
+ valueRef.current = nextValue;
32435
+ cursorRef.current = nextCursor;
32436
+ setValue(nextValue);
32437
+ setCursor(nextCursor);
32438
+ resetCursorBlink();
32439
+ preferredColumnRef.current = null;
32440
+ setCompletionIndex(0);
32441
+ setDismissedMenuValue(void 0);
32442
+ };
32443
+ const cancelImageSubmission = () => {
32444
+ prepareEpochRef.current += 1;
32445
+ prepareAbortRef.current?.abort();
32446
+ prepareAbortRef.current = void 0;
32447
+ setPreparingImages(false);
32448
+ dismissNotice();
32449
+ notify("image submission cancelled", "warning");
32450
+ };
32451
+ /** Move through visual rows first, then cross history at the true edge. */
32452
+ const navigateVertical = (direction) => {
32453
+ const currentValue = valueRef.current;
32454
+ const currentCursor = cursorRef.current;
32455
+ const model = editorModel(currentValue, editorColumns);
32456
+ const preferred = preferredColumnRef.current ?? caretSite(model, currentCursor).column;
32457
+ const next = moveCursorVertically(model, currentCursor, preferred, direction);
32458
+ if (next !== currentCursor) {
32459
+ cursorRef.current = next;
32460
+ setCursor(next);
32461
+ resetCursorBlink();
32462
+ preferredColumnRef.current = preferred;
32463
+ return;
32464
+ }
32465
+ if (recall.current.entries.length > 0 && shouldRecallNavigate(currentValue, currentCursor, recall.current.lastRecalled, direction)) {
32466
+ const step = direction < 0 ? recallOlder(recall.current, currentValue) : recallNewer(recall.current);
32467
+ recall.current = step.state;
32468
+ if (step.entry !== void 0) {
32469
+ const safe = sanitizeDraftText(step.entry);
32470
+ valueRef.current = safe;
32471
+ cursorRef.current = safe.length;
32472
+ setValue(safe);
32473
+ setCursor(safe.length);
32474
+ preferredColumnRef.current = null;
32475
+ setDismissedMenuValue(void 0);
32476
+ }
32477
+ }
32478
+ resetCursorBlink();
32479
+ };
32480
+ useStableInput((input, key) => {
32259
32481
  if (!active) return;
32260
- if (preparingImages) return;
32482
+ const liveValue = valueRef.current;
32483
+ const liveCursor = cursorRef.current;
32484
+ if (preparingImages) {
32485
+ if (key.escape || key.ctrl && input === "c") cancelImageSubmission();
32486
+ return;
32487
+ }
32261
32488
  if (deleteConfirm !== void 0) {
32262
32489
  if (input === "y" || input === "Y") confirmDelete();
32263
32490
  else cancelDelete();
@@ -32272,7 +32499,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32272
32499
  }
32273
32500
  return;
32274
32501
  }
32275
- if (key.ctrl && input === "r") {
32502
+ if ((key.ctrl || key.meta) && input === "r") {
32503
+ if (focusReporting && !terminalFocusedRef.current) return;
32276
32504
  toggleReasoning();
32277
32505
  return;
32278
32506
  }
@@ -32282,9 +32510,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32282
32510
  }
32283
32511
  if (key.ctrl && input === "c") {
32284
32512
  if (busy) interrupt();
32285
- else if (value !== "") {
32513
+ else if (liveValue !== "") {
32514
+ valueRef.current = "";
32515
+ cursorRef.current = 0;
32286
32516
  setValue("");
32287
32517
  setCursor(0);
32518
+ resetCursorBlink();
32288
32519
  draftImagesRef.current = [];
32289
32520
  setDraftImages([]);
32290
32521
  setCompletionIndex(0);
@@ -32293,8 +32524,8 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32293
32524
  return;
32294
32525
  }
32295
32526
  if (key.ctrl && input === "d") {
32296
- if (value !== "") {
32297
- applyEdit(deleteForward(value, cursor));
32527
+ if (liveValue !== "") {
32528
+ applyEdit(deleteForward(liveValue, liveCursor));
32298
32529
  return;
32299
32530
  }
32300
32531
  if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
@@ -32303,7 +32534,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32303
32534
  }
32304
32535
  if (key.escape) {
32305
32536
  if (menuActive) {
32306
- setDismissedMenuValue(value);
32537
+ setDismissedMenuValue(liveValue);
32307
32538
  return;
32308
32539
  }
32309
32540
  if (hasNotice) {
@@ -32313,27 +32544,33 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32313
32544
  if (busy) interrupt();
32314
32545
  return;
32315
32546
  }
32316
- if (key.delete && value === "" && queued.length > 0) {
32547
+ if (key.delete && liveValue === "" && queued.length > 0) {
32317
32548
  cancelQueued(queued[queued.length - 1].messageId);
32318
32549
  return;
32319
32550
  }
32320
32551
  if (key.return) {
32321
32552
  if (pasteBracketRef.current) {
32322
- applyEdit(insertText(value, cursor, "\n"));
32553
+ applyEdit(insertText(liveValue, liveCursor, "\n"));
32323
32554
  return;
32324
32555
  }
32325
32556
  if (menuActive) {
32326
- if (!(!mentionActive && candidates.some((candidate) => candidate.label === value))) {
32557
+ if (!(!mentionActive && candidates.some((candidate) => candidate.label === liveValue))) {
32327
32558
  acceptMenuCandidate();
32328
32559
  return;
32329
32560
  }
32330
32561
  }
32331
- const text = value.trim();
32562
+ const text = liveValue.trim();
32332
32563
  if (draftImagesRef.current.length > 0) {
32564
+ const controller = new AbortController();
32565
+ const epoch = prepareEpochRef.current + 1;
32566
+ prepareEpochRef.current = epoch;
32567
+ prepareAbortRef.current = controller;
32333
32568
  setPreparingImages(true);
32334
32569
  notify(`processing ${draftImagesRef.current.length} image${draftImagesRef.current.length === 1 ? "" : "s"}…`);
32335
32570
  const snapshot = draftImagesRef.current;
32336
- prepareImages(snapshot.map((image) => image.path)).then((images) => {
32571
+ prepareImages(snapshot.map((image) => image.path), controller.signal).then((images) => {
32572
+ if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
32573
+ prepareAbortRef.current = void 0;
32337
32574
  setPreparingImages(false);
32338
32575
  valueRef.current = "";
32339
32576
  cursorRef.current = 0;
@@ -32352,13 +32589,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32352
32589
  if (busy) steer(text, images);
32353
32590
  else dispatch(text, images);
32354
32591
  }, (reason) => {
32592
+ if (controller.signal.aborted || prepareEpochRef.current !== epoch) return;
32593
+ prepareAbortRef.current = void 0;
32355
32594
  setPreparingImages(false);
32356
32595
  notify(`image submission failed: ${reason instanceof Error ? reason.message : String(reason)}`, "error");
32357
32596
  });
32358
32597
  return;
32359
32598
  }
32599
+ valueRef.current = "";
32600
+ cursorRef.current = 0;
32360
32601
  setValue("");
32361
32602
  setCursor(0);
32603
+ resetCursorBlink();
32362
32604
  setCompletionIndex(0);
32363
32605
  setDismissedMenuValue(void 0);
32364
32606
  if (text === "") return;
@@ -32469,6 +32711,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32469
32711
  openTodos();
32470
32712
  return;
32471
32713
  }
32714
+ if (text === "/vscode-keys" || text.startsWith("/vscode-keys ")) {
32715
+ applyEditorKeys().then((summary) => notify(summary), (error) => notify(`vscode-keys failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
32716
+ return;
32717
+ }
32472
32718
  if (text === "/subagent") {
32473
32719
  openSubagent();
32474
32720
  return;
@@ -32485,6 +32731,12 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32485
32731
  return;
32486
32732
  }
32487
32733
  if (input === "\n" || input === "\r") return;
32734
+ if (menuActive && (key.tab || input.startsWith(" "))) {
32735
+ const remainder = key.tab ? "" : input.slice(1);
32736
+ acceptMenuCandidate();
32737
+ if (remainder !== "") applyEdit(insertText(valueRef.current, cursorRef.current, remainder));
32738
+ return;
32739
+ }
32488
32740
  if (menuActive && key.upArrow) {
32489
32741
  setCompletionIndex((index) => (index + menuRows.length - 1) % menuRows.length);
32490
32742
  return;
@@ -32493,119 +32745,74 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32493
32745
  setCompletionIndex((index) => (index + 1) % menuRows.length);
32494
32746
  return;
32495
32747
  }
32496
- const rawKey = rawAnnotation.current;
32497
- if (rawKey !== void 0) {
32498
- if (rawKey === "home") moveCursorTo(lineBounds(value, cursor).start);
32499
- else if (rawKey === "end") moveCursorTo(lineBounds(value, cursor).end);
32500
- else if (rawKey === "delete-backward") applyEdit(deleteBackward(value, cursor));
32501
- else if (rawKey === "delete-word-backward") applyEdit(deleteWordBackward(value, cursor));
32502
- else if (rawKey === "delete-forward") applyEdit(deleteForward(value, cursor));
32503
- else applyEdit(deleteWordForward(value, cursor));
32748
+ const rawTokens = rawEditorTokens.current;
32749
+ rawEditorTokens.current = void 0;
32750
+ if (rawTokens !== void 0) {
32751
+ applyRawEditorTokens(rawTokens);
32504
32752
  return;
32505
32753
  }
32506
32754
  if (key.upArrow || key.downArrow) {
32507
- if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
32508
- const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current);
32509
- recall.current = step.state;
32510
- if (step.entry !== void 0) {
32511
- const safe = sanitizeDraftText(step.entry);
32512
- setValue(safe);
32513
- setCursor(safe.length);
32514
- preferredColumnRef.current = null;
32515
- setDismissedMenuValue(void 0);
32516
- }
32517
- return;
32518
- }
32519
- const model = editorModel(value, Math.max(1, columns - 6));
32520
- const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
32521
- const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1);
32522
- if (next !== cursor) {
32523
- setCursor(next);
32524
- preferredColumnRef.current = preferred;
32525
- }
32755
+ navigateVertical(key.upArrow ? -1 : 1);
32526
32756
  return;
32527
32757
  }
32528
32758
  if (key.ctrl && (input === "p" || input === "n")) {
32529
- const up = input === "p";
32530
- if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
32531
- const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current);
32532
- recall.current = step.state;
32533
- if (step.entry !== void 0) {
32534
- const safe = sanitizeDraftText(step.entry);
32535
- setValue(safe);
32536
- setCursor(safe.length);
32537
- preferredColumnRef.current = null;
32538
- setDismissedMenuValue(void 0);
32539
- }
32540
- return;
32541
- }
32542
- const model = editorModel(value, Math.max(1, columns - 6));
32543
- const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
32544
- const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1);
32545
- if (next !== cursor) {
32546
- setCursor(next);
32547
- preferredColumnRef.current = preferred;
32548
- }
32549
- return;
32550
- }
32551
- if (key.tab && menuActive) {
32552
- acceptMenuCandidate();
32759
+ navigateVertical(input === "p" ? -1 : 1);
32553
32760
  return;
32554
32761
  }
32555
32762
  if (key.leftArrow) {
32556
- moveCursorTo(key.meta || key.ctrl ? moveWordLeft(value, cursor) : moveCursorBy(value, cursor, -1));
32763
+ moveCursorTo(key.meta || key.ctrl ? moveWordLeft(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, -1));
32557
32764
  return;
32558
32765
  }
32559
32766
  if (key.rightArrow) {
32560
- moveCursorTo(key.meta || key.ctrl ? moveWordRight(value, cursor) : moveCursorBy(value, cursor, 1));
32767
+ moveCursorTo(key.meta || key.ctrl ? moveWordRight(liveValue, liveCursor) : moveCursorBy(liveValue, liveCursor, 1));
32561
32768
  return;
32562
32769
  }
32563
32770
  if (key.meta && input === "b") {
32564
- moveCursorTo(moveWordLeft(value, cursor));
32771
+ moveCursorTo(moveWordLeft(liveValue, liveCursor));
32565
32772
  return;
32566
32773
  }
32567
32774
  if (key.meta && input === "f") {
32568
- moveCursorTo(moveWordRight(value, cursor));
32775
+ moveCursorTo(moveWordRight(liveValue, liveCursor));
32569
32776
  return;
32570
32777
  }
32571
32778
  if (key.ctrl && input === "b") {
32572
- moveCursorTo(moveCursorBy(value, cursor, -1));
32779
+ moveCursorTo(moveCursorBy(liveValue, liveCursor, -1));
32573
32780
  return;
32574
32781
  }
32575
32782
  if (key.ctrl && input === "f") {
32576
- moveCursorTo(moveCursorBy(value, cursor, 1));
32783
+ moveCursorTo(moveCursorBy(liveValue, liveCursor, 1));
32577
32784
  return;
32578
32785
  }
32579
32786
  if (key.ctrl && input === "w") {
32580
- applyEdit(deleteWordBackward(value, cursor));
32787
+ applyEdit(deleteWordBackward(liveValue, liveCursor));
32581
32788
  return;
32582
32789
  }
32583
32790
  if (key.meta && input === "d") {
32584
- applyEdit(deleteWordForward(value, cursor));
32791
+ applyEdit(deleteWordForward(liveValue, liveCursor));
32585
32792
  return;
32586
32793
  }
32587
32794
  if (key.backspace || key.delete) {
32588
- applyEdit(deleteBackward(value, cursor));
32795
+ applyEdit(deleteBackward(liveValue, liveCursor));
32589
32796
  return;
32590
32797
  }
32591
32798
  if (key.ctrl && input === "a") {
32592
- moveCursorTo(lineBounds(value, cursor).start);
32799
+ moveCursorTo(moveToLineStart(liveValue, liveCursor, true));
32593
32800
  return;
32594
32801
  }
32595
32802
  if (key.ctrl && input === "e") {
32596
- moveCursorTo(lineBounds(value, cursor).end);
32803
+ moveCursorTo(moveToLineEnd(liveValue, liveCursor, true));
32597
32804
  return;
32598
32805
  }
32599
32806
  if (key.ctrl && input === "u") {
32600
- applyEdit(killToLineStart(value, cursor));
32807
+ applyEdit(killToLineStart(liveValue, liveCursor));
32601
32808
  return;
32602
32809
  }
32603
32810
  if (key.ctrl && input === "k") {
32604
- applyEdit(killToLineEnd(value, cursor));
32811
+ applyEdit(killToLineEnd(liveValue, liveCursor));
32605
32812
  return;
32606
32813
  }
32607
32814
  if (key.ctrl && input === "y") {
32608
- if (killRef.current !== "") applyEdit(insertText(value, cursor, killRef.current));
32815
+ if (killRef.current !== "") applyEdit(insertText(liveValue, liveCursor, killRef.current));
32609
32816
  return;
32610
32817
  }
32611
32818
  if (key.ctrl && input === "l") {
@@ -32638,9 +32845,9 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32638
32845
  insertDroppedImages(droppedPaths);
32639
32846
  return;
32640
32847
  }
32641
- applyEdit(insertText(value, cursor, text));
32848
+ applyEdit(insertText(valueRef.current, cursorRef.current, text));
32642
32849
  }
32643
- });
32850
+ }, active);
32644
32851
  const [waveTick, setWaveTick] = (0, import_react.useState)(null);
32645
32852
  const wavePrevious = (0, import_react.useRef)({
32646
32853
  tier: null,
@@ -32658,7 +32865,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32658
32865
  }
32659
32866
  if (previous.tier !== waveTier || previous.style !== waveStyle) setWaveTick(0);
32660
32867
  }, [waveTier, waveStyle]);
32661
- const waveActive = waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
32868
+ const waveActive = !preparingImages && waveTick !== null && waveTier !== null && waveStyle !== null && waveTick * 33 < deepseekWaveDuration(waveTier, waveStyle);
32662
32869
  (0, import_react.useEffect)(() => {
32663
32870
  if (!waveActive) return;
32664
32871
  const id = setInterval(() => {
@@ -32679,7 +32886,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32679
32886
  const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
32680
32887
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
32681
32888
  const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
32682
- const editorViewModel = editorModel(value, Math.max(1, columns - 6));
32889
+ const editorViewModel = editorModel(value, editorColumns);
32683
32890
  const clampedCursor = clampCursor(value, cursor);
32684
32891
  const caret = caretSite(editorViewModel, clampedCursor);
32685
32892
  const editorWindowRows = Math.min(editorViewModel.rows.length, Math.max(1, maxRows));
@@ -32730,123 +32937,120 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
32730
32937
  const editorRows = [];
32731
32938
  for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
32732
32939
  const row = editorViewModel.rows[index];
32733
- const caretAt = index === caret.row ? row.offsets.indexOf(clampedCursor) : -1;
32734
- const before = caretAt > 0 ? row.text.slice(0, row.cuts[caretAt]) : "";
32735
- const caretChar = caretAt >= 0 && caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt], row.cuts[caretAt + 1]) : " ";
32736
- const after = caretAt < 0 ? row.text : caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt + 1]) : "";
32737
- const placeholder = index === 0 && value === "" && !busy;
32738
- const tail = placeholder ? COMPOSER_PLACEHOLDER : after;
32739
- const consumed = 2 + visibleColumns(before) + visibleColumns(caretChar) + visibleColumns(tail);
32940
+ const parts = editorRowParts(row, index, caret.row, clampedCursor, !preparingImages);
32941
+ const placeholder = index === 0 && value === "" && !busy && !preparingImages;
32942
+ const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
32943
+ const consumed = 2 + visibleColumns(parts.before) + visibleColumns(parts.caret) + visibleColumns(tail);
32740
32944
  editorRows.push((0, import_react.createElement)(Text, {
32741
32945
  key: index,
32742
32946
  backgroundColor: bandBg,
32743
32947
  wrap: "truncate-end"
32744
- }, index === 0 ? busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
32948
+ }, index === 0 ? preparingImages ? (0, import_react.createElement)(Text, {
32949
+ color: inkColor(getPalette().warn),
32950
+ bold: true
32951
+ }, "… ") : busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
32745
32952
  color: promptColor,
32746
32953
  bold: tierActive ? true : void 0
32747
- }, `${promptGlyph} `) : " ", before, (0, import_react.createElement)(CursorBlock, {
32954
+ }, `${promptGlyph} `) : " ", parts.before, parts.hasCaret ? (0, import_react.createElement)(Text, {
32748
32955
  key: "caret",
32749
- char: caretChar
32750
- }), placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : after, bandFill(consumed)));
32956
+ inverse: cursorVisible || void 0
32957
+ }, parts.caret) : null, placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : parts.after, bandFill(consumed)));
32751
32958
  }
32752
32959
  const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
32753
32960
  const waveRow = () => {
32754
32961
  const hues = deepseekWaveHues(waveTier);
32755
32962
  const style = waveStyle;
32756
32963
  const bandRgb = getPalette().composerBand;
32964
+ const visibleRows = editorViewModel.rows.slice(editorWindowStart, editorWindowStart + editorWindowRows);
32965
+ const totalBandRows = visibleRows.length + 2;
32757
32966
  const waveBg = (row, column) => {
32758
- const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, 3);
32967
+ const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, totalBandRows);
32759
32968
  return rgb === null ? bandBg : inkColor(rgb);
32760
32969
  };
32761
32970
  const blankBandRow = (row) => {
32762
32971
  const blanks = [];
32763
- while (blanks.length < bandWidth) blanks.push({
32972
+ for (let column = 0; column < bandWidth; column += 1) blanks.push({
32764
32973
  char: " ",
32765
- backgroundColor: waveBg(row, blanks.length)
32974
+ width: 1,
32975
+ backgroundColor: waveBg(row, column)
32766
32976
  });
32767
- return (0, import_react.createElement)(Text, { key: row }, ...waveRowSpans(blanks));
32977
+ return (0, import_react.createElement)(Text, { key: `blank-${row}` }, ...waveRowSpans(blanks));
32768
32978
  };
32769
- const waveEditor = editorWindow(value, cursor, Math.max(1, columns - 7));
32770
- const cells = [
32771
- {
32772
- char: " ",
32773
- backgroundColor: waveBg(1, 0)
32774
- },
32775
- {
32776
- char: " ",
32777
- backgroundColor: waveBg(1, 1)
32778
- },
32779
- {
32780
- char: promptGlyph,
32781
- color: promptColor,
32782
- bold: true,
32783
- backgroundColor: waveBg(1, 2)
32784
- },
32785
- {
32786
- char: " ",
32787
- color: promptColor,
32788
- backgroundColor: waveBg(1, 3)
32979
+ const cellIndexAtColumn = (cells, target) => {
32980
+ let column = 0;
32981
+ for (let index = 0; index < cells.length; index += 1) {
32982
+ if (column === target) return index;
32983
+ column += cells[index].width ?? visibleColumns(cells[index].char);
32984
+ if (column > target) return void 0;
32789
32985
  }
32790
- ];
32791
- for (const char of waveEditor.before) cells.push({
32792
- char,
32793
- backgroundColor: waveBg(1, cells.length)
32794
- });
32795
- cells.push({
32796
- char: waveEditor.caret,
32797
- inverse: true,
32798
- backgroundColor: waveBg(1, cells.length)
32799
- });
32800
- if (value === "" && !busy) for (let at = 0; at < 40; at += 1) cells.push({
32801
- char: COMPOSER_PLACEHOLDER[at],
32802
- dim: true,
32803
- backgroundColor: waveBg(1, cells.length)
32804
- });
32805
- else for (const char of waveEditor.after) cells.push({
32806
- char,
32807
- backgroundColor: waveBg(1, cells.length)
32808
- });
32809
- while (cells.length < bandWidth) cells.push({
32810
- char: " ",
32811
- backgroundColor: waveBg(1, cells.length)
32986
+ };
32987
+ const editorWaveRows = visibleRows.map((row, visibleIndex) => {
32988
+ const sourceIndex = editorWindowStart + visibleIndex;
32989
+ const bandRow = visibleIndex + 1;
32990
+ const parts = editorRowParts(row, sourceIndex, caret.row, clampedCursor);
32991
+ const placeholder = sourceIndex === 0 && value === "" && !busy;
32992
+ const cells = [];
32993
+ let usedColumns = 0;
32994
+ const push = (char, extra = {}) => {
32995
+ const width = visibleColumns(char);
32996
+ cells.push({
32997
+ char,
32998
+ width,
32999
+ backgroundColor: waveBg(bandRow, usedColumns),
33000
+ ...extra
33001
+ });
33002
+ usedColumns += width;
33003
+ };
33004
+ if (sourceIndex === 0) {
33005
+ push(promptGlyph, {
33006
+ color: promptColor,
33007
+ bold: true
33008
+ });
33009
+ push(" ", { color: promptColor });
33010
+ } else {
33011
+ push(" ");
33012
+ push(" ");
33013
+ }
33014
+ for (const span of splitGraphemes(parts.before)) push(span.text);
33015
+ if (parts.hasCaret) push(parts.caret, { inverse: cursorVisible });
33016
+ const tail = placeholder ? COMPOSER_PLACEHOLDER : parts.after;
33017
+ for (const span of splitGraphemes(tail)) push(span.text, placeholder ? { dim: true } : {});
33018
+ while (usedColumns < bandWidth) push(" ");
33019
+ const middleBandRow = Math.floor(totalBandRows / 2);
33020
+ if (bandRow === middleBandRow && deepseekWaveWordVisible(waveTick, waveTier, style)) {
33021
+ const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
33022
+ const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
33023
+ const indices = Array.from({ length: word.length }, (_, at) => cellIndexAtColumn(cells, start + at));
33024
+ if (indices.every((index) => index !== void 0 && (cells[index].char === " " || cells[index].dim === true))) for (let at = 0; at < word.length; at += 1) {
33025
+ const cell = cells[indices[at]];
33026
+ cell.char = word[at];
33027
+ cell.width = 1;
33028
+ cell.color = inkColor(deepseekWaveWordHue(at, hues));
33029
+ cell.bold = true;
33030
+ cell.dim = false;
33031
+ }
33032
+ }
33033
+ if (bandRow === middleBandRow && (waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
33034
+ const spark = deepseekWaveSpark(waveTick);
33035
+ const lastIndex = cellIndexAtColumn(cells, bandWidth - 1);
33036
+ if (spark !== null && lastIndex !== void 0 && cells[lastIndex].char === " ") {
33037
+ cells[lastIndex].char = spark;
33038
+ cells[lastIndex].color = promptColor;
33039
+ cells[lastIndex].bold = true;
33040
+ cells[lastIndex].dim = false;
33041
+ }
33042
+ }
33043
+ return (0, import_react.createElement)(Text, {
33044
+ key: `editor-${sourceIndex}`,
33045
+ wrap: "truncate-end"
33046
+ }, ...waveRowSpans(cells));
32812
33047
  });
32813
- if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
32814
- const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
32815
- const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
32816
- let clear = true;
32817
- for (let at = 0; at < word.length; at += 1) {
32818
- const cell = cells[start + at];
32819
- if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
32820
- clear = false;
32821
- break;
32822
- }
32823
- }
32824
- if (clear) for (let at = 0; at < word.length; at += 1) {
32825
- const cell = cells[start + at];
32826
- cell.char = word[at];
32827
- cell.color = inkColor(deepseekWaveWordHue(at, hues));
32828
- cell.bold = true;
32829
- cell.dim = false;
32830
- }
32831
- }
32832
- if ((waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
32833
- const spark = deepseekWaveSpark(waveTick);
32834
- if (spark !== null) {
32835
- const last = cells[cells.length - 1];
32836
- if (last !== void 0 && last.char === " ") {
32837
- last.char = spark;
32838
- last.color = promptColor;
32839
- last.bold = true;
32840
- last.dim = false;
32841
- }
32842
- }
32843
- }
32844
33048
  return (0, import_react.createElement)(Box, {
32845
33049
  flexDirection: "column",
32846
33050
  width: bandWidth
32847
- }, blankBandRow(0), (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...waveRowSpans(cells)), blankBandRow(2));
33051
+ }, blankBandRow(0), ...editorWaveRows, blankBandRow(totalBandRows - 1));
32848
33052
  };
32849
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : band(staticEditor));
33053
+ return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && waveStyle !== null && !busy && !preparingImages ? waveRow() : band(staticEditor));
32850
33054
  }
32851
33055
  /** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
32852
33056
  function buildSettledRow(entry, index, showReasoning, columns) {
@@ -33143,6 +33347,7 @@ function App(props) {
33143
33347
  }, [modelOpen, props.subscribeProviderAuthorizations]);
33144
33348
  const busy = view.busy;
33145
33349
  const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
33350
+ const budgetWarnRef = (0, import_react.useRef)(void 0);
33146
33351
  const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
33147
33352
  const [diffView, setDiffView] = (0, import_react.useState)(void 0);
33148
33353
  const [helpOpen, setHelpOpen] = (0, import_react.useState)(false);
@@ -33311,6 +33516,18 @@ function App(props) {
33311
33516
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
33312
33517
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
33313
33518
  const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
33519
+ const liveAudit = clampLiveAllocation({
33520
+ live: visibleLiveLines.length,
33521
+ reasoning: reasoningRows,
33522
+ answer: answerRows
33523
+ }, dynamicRows);
33524
+ if (liveAudit.warning !== void 0 && budgetWarnRef.current !== liveAudit.warning) {
33525
+ budgetWarnRef.current = liveAudit.warning;
33526
+ console.warn(`[dsh-code] ${liveAudit.warning}`);
33527
+ }
33528
+ const auditedLiveLines = liveAudit.allocation.live === visibleLiveLines.length ? visibleLiveLines : visibleLiveLines.slice(-liveAudit.allocation.live);
33529
+ const auditedReasoningRows = liveAudit.allocation.reasoning;
33530
+ const auditedAnswerRows = liveAudit.allocation.answer;
33314
33531
  const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
33315
33532
  const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || verboseOpen && !approvalPending && !questionPending || diffView !== void 0 || approvalPending || questionPending;
33316
33533
  const closeInspector = (0, import_react.useCallback)(() => {
@@ -33548,18 +33765,27 @@ function App(props) {
33548
33765
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
33549
33766
  key: refreshEpoch,
33550
33767
  items: settledRows
33551
- }), transcriptVisible ? (0, import_react.createElement)(Box, { flexDirection: "column" }, visibleLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: visibleLiveLines }), view.streamingReasoning !== "" && reasoningRows > 0 ? (0, import_react.createElement)(StreamTail, {
33552
- text: showReasoning ? view.streamingReasoning : "Thinking… (Ctrl+R to expand)",
33768
+ }), transcriptVisible ? (0, import_react.createElement)(Box, { flexDirection: "column" }, auditedLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: auditedLiveLines }), view.streamingReasoning !== "" && auditedReasoningRows > 0 ? showReasoning ? (0, import_react.createElement)(StreamTail, {
33769
+ text: view.streamingReasoning,
33770
+ prefix: "✻ ",
33771
+ continuationPrefix: " ",
33772
+ dim: true,
33773
+ maxRows: auditedReasoningRows
33774
+ }) : view.streaming === "" ? (0, import_react.createElement)(ShimmerLine, { text: "✻ Thinking… (Ctrl/Alt+R to expand)" }) : (0, import_react.createElement)(StreamTail, {
33775
+ text: "Thinking… (Ctrl/Alt+R to expand)",
33553
33776
  prefix: "✻ ",
33554
33777
  continuationPrefix: " ",
33555
33778
  dim: true,
33556
- maxRows: reasoningRows
33557
- }) : void 0, view.streaming !== "" && answerRows > 0 ? (0, import_react.createElement)(StreamTail, {
33779
+ maxRows: auditedReasoningRows
33780
+ }) : void 0, view.streaming !== "" && auditedAnswerRows > 0 ? (0, import_react.createElement)(StreamTail, {
33558
33781
  text: view.streaming,
33559
33782
  dim: false,
33560
- maxRows: answerRows,
33783
+ maxRows: auditedAnswerRows,
33561
33784
  prefix: " "
33562
- }, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, { rows: agentRows }) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
33785
+ }, busy ? (0, import_react.createElement)(Caret) : void 0) : void 0, deepDivingVisible ? (0, import_react.createElement)(DeepDivingLine, { since: view.busySince }) : void 0) : void 0, transcriptVisible ? (0, import_react.createElement)(TodoPanel, { todos: view.todos }) : void 0, transcriptVisible ? (0, import_react.createElement)(AgentsLine, {
33786
+ rows: agentRows,
33787
+ total: props.subagents.getTotalSeen()
33788
+ }) : void 0, todosOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoTodoListPanel, {
33563
33789
  todos: view.todos,
33564
33790
  onClose: () => {
33565
33791
  setTodosOpen(false);
@@ -33692,6 +33918,7 @@ function App(props) {
33692
33918
  descriptors,
33693
33919
  skills,
33694
33920
  dispatch: props.dispatch,
33921
+ applyEditorKeys: props.applyEditorKeys,
33695
33922
  steer: props.steer,
33696
33923
  interrupt: props.interrupt,
33697
33924
  quit: props.quit,
@@ -33786,7 +34013,7 @@ function App(props) {
33786
34013
  refresh: refreshScreen,
33787
34014
  toggleReasoning: () => {
33788
34015
  setShowReasoning((current) => !current);
33789
- if (!busy && !streamingActive) refreshScreen();
34016
+ refreshScreen();
33790
34017
  },
33791
34018
  loadMentions: props.loadMentions,
33792
34019
  inspectImages: props.inspectImages,
@@ -33992,7 +34219,9 @@ function isSlashLine(line) {
33992
34219
  /** Substitutable runner effects; production values write to the real terminal. */
33993
34220
  const internals = {
33994
34221
  mount: (element) => {
33995
- process.stdout.write("\x1B[>4;0m\x1B[>5u\x1B[?2004h");
34222
+ const keyboardEnhanced = shouldEnableKeyboardEnhancement();
34223
+ const focusReporting = isVsCodeTerminalEnv();
34224
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_ENABLE : "") + BRACKETED_PASTE_ENABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_ENABLE : ""));
33996
34225
  const instance = render(element, { exitOnCtrlC: false });
33997
34226
  return {
33998
34227
  rerender(element) {
@@ -34000,7 +34229,7 @@ const internals = {
34000
34229
  },
34001
34230
  unmount() {
34002
34231
  instance.unmount();
34003
- process.stdout.write("\x1B[<u\x1B[>4;0m\x1B[?2004l");
34232
+ process.stdout.write((keyboardEnhanced ? KEYBOARD_ENHANCE_DISABLE : "") + BRACKETED_PASTE_DISABLE + (focusReporting ? TERMINAL_FOCUS_REPORT_DISABLE : ""));
34004
34233
  }
34005
34234
  };
34006
34235
  },
@@ -34153,6 +34382,202 @@ async function loadModelDirectory(ctx) {
34153
34382
  };
34154
34383
  }
34155
34384
  //#endregion
34385
+ //#region src/model-capabilities.ts
34386
+ /** The settings namespace the llm-pi-ai plugin owns (identity via `settingsNamespace`). */
34387
+ const PI_AI_SETTINGS_NS = "llm-pi-ai";
34388
+ /** The effort level that means "send no reasoning parameter"; its wire value is always null. */
34389
+ const OFF_LEVEL = "off";
34390
+ /** Single-line a misbehaving error for the notice channel. */
34391
+ function singleLine$1(message) {
34392
+ return [...message].map((ch) => {
34393
+ const code = ch.charCodeAt(0);
34394
+ return code < 32 || code === 127 ? " " : ch;
34395
+ }).join("").split(" ").filter((part) => part !== "").join(" ");
34396
+ }
34397
+ /** Human text for a rejection value. */
34398
+ function messageOf$1(error) {
34399
+ return error instanceof Error ? error.message : String(error);
34400
+ }
34401
+ /** Read the value at a path through plain objects; undefined when any segment misses. */
34402
+ function getPath$1(value, path) {
34403
+ let current = value;
34404
+ for (const segment of path) {
34405
+ if (typeof current !== "object" || current === null) return void 0;
34406
+ current = current[segment];
34407
+ }
34408
+ return current;
34409
+ }
34410
+ /** Whether a raw settings value is a usable `reasoningEfforts` dict (non-empty, non-false). */
34411
+ function isDeclaredEfforts(value) {
34412
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
34413
+ }
34414
+ /**
34415
+ * The identity level map for an advertised effort list: `off` sends no
34416
+ * parameter, every other level keeps its own name — the wire spelling
34417
+ * OpenAI-compatible relays accept as-is.
34418
+ */
34419
+ function identityEfforts(levels) {
34420
+ const dict = {};
34421
+ for (const level of levels) dict[level] = level === OFF_LEVEL ? null : level;
34422
+ return dict;
34423
+ }
34424
+ /**
34425
+ * Plan the reasoning declarations to materialize. A model entry inherits
34426
+ * when it declares nothing (`reasoningEfforts` absent — a dict or `false` is
34427
+ * an explicit choice and is never touched) and its live row advertises no
34428
+ * efforts; the donor is the first sibling settings declaration for the same
34429
+ * id, copied verbatim so dialect wire spellings survive, otherwise the first
34430
+ * other-route row advertising efforts for that id, mapped by identity.
34431
+ * Entries never lose fields and keep their key order; a provider appears in
34432
+ * the result only when at least one entry changes.
34433
+ * @param input - the live model rows and the raw pi-ai profiles from settings.
34434
+ * @returns one plan per provider with at least one inheritance.
34435
+ */
34436
+ function planCapabilitySync(input) {
34437
+ const declared = /* @__PURE__ */ new Map();
34438
+ for (const [provider, profile] of input.profiles) for (const entry of profile.models) {
34439
+ const efforts = entry.reasoningEfforts;
34440
+ if (!isDeclaredEfforts(efforts)) continue;
34441
+ if (!declared.has(String(entry.id))) declared.set(String(entry.id), {
34442
+ dict: efforts,
34443
+ provider
34444
+ });
34445
+ }
34446
+ const advertised = /* @__PURE__ */ new Map();
34447
+ for (const row of input.rows) {
34448
+ const levels = row.reasoning?.efforts.map((effort) => effort.id) ?? [];
34449
+ if (levels.filter((level) => level !== OFF_LEVEL).length === 0) continue;
34450
+ if (!advertised.has(row.model)) advertised.set(row.model, {
34451
+ levels,
34452
+ provider: row.provider
34453
+ });
34454
+ }
34455
+ const plans = [];
34456
+ for (const [provider, profile] of input.profiles) {
34457
+ const inherited = [];
34458
+ const sources = [];
34459
+ const models = profile.models.map((entry) => {
34460
+ if (entry.reasoningEfforts !== void 0) return entry;
34461
+ const id = String(entry.id);
34462
+ const sibling = declared.get(id);
34463
+ if (sibling !== void 0 && sibling.provider !== provider) {
34464
+ inherited.push(id);
34465
+ if (!sources.includes(`${sibling.provider}/${id}`)) sources.push(`${sibling.provider}/${id}`);
34466
+ return {
34467
+ ...entry,
34468
+ reasoningEfforts: sibling.dict
34469
+ };
34470
+ }
34471
+ const donor = advertised.get(id);
34472
+ if (donor !== void 0 && donor.provider !== provider) {
34473
+ inherited.push(id);
34474
+ if (!sources.includes(`${donor.provider}/${id}`)) sources.push(`${donor.provider}/${id}`);
34475
+ return {
34476
+ ...entry,
34477
+ reasoningEfforts: identityEfforts(donor.levels)
34478
+ };
34479
+ }
34480
+ return entry;
34481
+ });
34482
+ if (inherited.length > 0) plans.push({
34483
+ provider,
34484
+ settingsNs: profile.settingsNs,
34485
+ settingsPath: profile.settingsPath,
34486
+ models,
34487
+ sourceFingerprint: JSON.stringify(profile.models),
34488
+ sourceRevision: profile.revision,
34489
+ inherited,
34490
+ sources
34491
+ });
34492
+ }
34493
+ return plans;
34494
+ }
34495
+ /** Provider -> the source snapshot and document revision its last write landed against. */
34496
+ const lastApplied = /* @__PURE__ */ new Map();
34497
+ /** Fresh revision of one namespace, or undefined when it does not resolve. */
34498
+ function revisionOf(settings, ns) {
34499
+ try {
34500
+ for (const descriptor of settings.describe({ redactSecrets: true })) if (descriptor.ns === ns) return descriptor.revision;
34501
+ } catch {
34502
+ return;
34503
+ }
34504
+ }
34505
+ /**
34506
+ * Materialize same-id reasoning declarations once per provider. Reads the
34507
+ * configurable directory, the redacted settings document, and the live model
34508
+ * rows; plans; then writes each provider's merged models array through
34509
+ * `settings.mutate` under a fresh revision (writes bump the section
34510
+ * revision, so per-plan revisions are re-read). Every failure converges to a
34511
+ * single-line notice — the caller's promise never rejects and the session
34512
+ * keeps running on the previous configuration.
34513
+ * @param ctx - context carrying the `llm` and `settings` services (optional).
34514
+ * @param notify - the app bridge's notice sink, when one is live.
34515
+ */
34516
+ async function syncModelCapabilities(ctx, notify) {
34517
+ try {
34518
+ const llm = ctx.get("llm");
34519
+ if (llm?.listConfigurableProviders === void 0) return;
34520
+ const settings = ctx.get("settings");
34521
+ if (settings === void 0) return;
34522
+ let entries;
34523
+ try {
34524
+ entries = llm.listConfigurableProviders();
34525
+ } catch {
34526
+ return;
34527
+ }
34528
+ const routed = entries.filter((entry) => entry.settingsNs === PI_AI_SETTINGS_NS);
34529
+ if (routed.length === 0) return;
34530
+ let descriptors;
34531
+ try {
34532
+ descriptors = settings.describe({ redactSecrets: true });
34533
+ } catch {
34534
+ return;
34535
+ }
34536
+ const namespaces = new Map(descriptors.map((descriptor) => [descriptor.ns, descriptor]));
34537
+ const profiles = /* @__PURE__ */ new Map();
34538
+ for (const entry of routed) {
34539
+ const namespace = namespaces.get(entry.settingsNs);
34540
+ if (namespace === void 0) continue;
34541
+ const profile = entry.settingsPath.length === 0 ? namespace.value : getPath$1(namespace.value, entry.settingsPath);
34542
+ const models = Array.isArray(profile?.models) ? profile.models : void 0;
34543
+ if (models === void 0) continue;
34544
+ profiles.set(entry.provider, {
34545
+ settingsNs: entry.settingsNs,
34546
+ settingsPath: entry.settingsPath,
34547
+ revision: namespace.revision,
34548
+ models
34549
+ });
34550
+ }
34551
+ if (profiles.size === 0) return;
34552
+ const directory = await loadModelDirectory(ctx);
34553
+ for (const plan of planCapabilitySync({
34554
+ rows: directory.rows,
34555
+ profiles
34556
+ })) {
34557
+ const applied = lastApplied.get(plan.provider);
34558
+ if (applied !== void 0 && applied.fingerprint === plan.sourceFingerprint && applied.revision === plan.sourceRevision) continue;
34559
+ const revision = revisionOf(settings, plan.settingsNs);
34560
+ if (revision === void 0) continue;
34561
+ try {
34562
+ await settings.mutate(plan.settingsNs, [{
34563
+ op: "set",
34564
+ path: [...plan.settingsPath, "models"],
34565
+ value: plan.models
34566
+ }], revision);
34567
+ lastApplied.set(plan.provider, {
34568
+ fingerprint: plan.sourceFingerprint,
34569
+ revision: plan.sourceRevision
34570
+ });
34571
+ const shown = plan.sources.slice(0, 3).join(", ");
34572
+ const more = plan.sources.length > 3 ? `, +${plan.sources.length - 3}` : "";
34573
+ notify?.(`inherited reasoning levels for ${plan.inherited.length} model${plan.inherited.length === 1 ? "" : "s"} on ${plan.provider} (from ${shown}${more})`, "info");
34574
+ } catch (error) {
34575
+ notify?.(`capability inheritance failed on ${plan.provider}: ${singleLine$1(messageOf$1(error))}`, "warning");
34576
+ }
34577
+ }
34578
+ } catch {}
34579
+ }
34580
+ //#endregion
34156
34581
  //#region src/provider-settings.ts
34157
34582
  /** Human text for a rejection value (mirrors the web page's `messageOf`). */
34158
34583
  function messageOf(error) {
@@ -34201,11 +34626,13 @@ function configurationOf(profile) {
34201
34626
  if (typeof value !== "object" || value === null) return [];
34202
34627
  const entry = value;
34203
34628
  if (typeof entry.id !== "string" || entry.id.trim() === "") return [];
34629
+ const { id: _id, name: _name, contextWindow: _contextWindow, maxTokens: _maxTokens, ...extras } = entry;
34204
34630
  return [{
34205
34631
  id: entry.id,
34206
34632
  ...typeof entry.name === "string" && entry.name.trim() !== "" ? { name: entry.name } : {},
34207
34633
  ...typeof entry.contextWindow === "number" && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {},
34208
- ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {}
34634
+ ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {},
34635
+ ...Object.keys(extras).length > 0 ? { extras } : {}
34209
34636
  }];
34210
34637
  });
34211
34638
  return {
@@ -34421,6 +34848,7 @@ async function saveProviderConfiguration(ctx, target, configuration) {
34421
34848
  for (const [label, value] of [["context window", model.contextWindow], ["output window", model.maxTokens]]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new ProviderSettingsError(`${label} must be a positive integer`);
34422
34849
  return {
34423
34850
  id,
34851
+ ...model.extras,
34424
34852
  ...model.name === void 0 || model.name.trim() === "" ? {} : { name: model.name.trim() },
34425
34853
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
34426
34854
  ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
@@ -34504,83 +34932,6 @@ async function removeProviderSettings(ctx, target) {
34504
34932
  }
34505
34933
  }
34506
34934
  //#endregion
34507
- //#region src/mentions.ts
34508
- /** Menu cap on file rows; the service owns ranking and default rows. */
34509
- const MAX_FILE_ROWS = 20;
34510
- /**
34511
- * Create the mention API for one agent's workspace. A missing
34512
- * `fileReferences` service (with an agent present) or `sessionReferenceResolver`
34513
- * degrades that half to empty rows; `prepare` passes text through untouched
34514
- * without references. An undefined agent (a bare launch before any session
34515
- * exists) runs the official WorkspaceFileSearch over the launch cwd — the
34516
- * same class the mounted service uses per agent — so `@` file completion
34517
- * works from the first keystroke; session references wait for the session.
34518
- *
34519
- * `candidates` never reaches for `this` — the runner hands it to the input
34520
- * editor as a detached callback, and a `this`-bound method would throw on
34521
- * every `@` key.
34522
- * @param ctx - context carrying the optional `fileReferences` and
34523
- * `sessionReferenceResolver` services.
34524
- * @param agent - the session owner; excluded from its own session candidates.
34525
- * @param cwd - launch working directory; bounds the pre-session search.
34526
- */
34527
- function createMentions(ctx, agent, cwd) {
34528
- const resolver = ctx.get("sessionReferenceResolver");
34529
- const fileReferences = ctx.get("fileReferences");
34530
- const sessionCapable = agent !== void 0 && resolver !== void 0;
34531
- let preSessionSearch;
34532
- const preSessionFiles = (query, signal) => {
34533
- preSessionSearch ??= new WorkspaceFileSearch(cwd, {
34534
- maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
34535
- maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
34536
- excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
34537
- });
34538
- return preSessionSearch.list(query, signal ?? new AbortController().signal);
34539
- };
34540
- return {
34541
- async candidates(query, signal) {
34542
- const needle = query.trim();
34543
- const [files, sessions] = await Promise.all([agent !== void 0 && fileReferences !== void 0 ? fileReferences.list(agent, needle, signal ?? new AbortController().signal).catch(() => []) : agent === void 0 ? preSessionFiles(needle, signal).catch(() => []) : Promise.resolve([]), sessionCapable && needle !== "" && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
34544
- const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
34545
- label: candidate.path,
34546
- description: candidate.kind === "directory" ? "Folder" : "File",
34547
- kind: candidate.kind,
34548
- ...candidate.kind === "file" ? { path: isAbsolute(candidate.path) ? candidate.path : resolve(cwd, candidate.path) } : {}
34549
- }));
34550
- const sessionRows = sessions.map((candidate) => ({
34551
- label: formatSessionReferenceMention(candidate),
34552
- description: `Session · ${candidate.cwd ?? "(no cwd)"}`,
34553
- kind: "session"
34554
- }));
34555
- return [...fileRows, ...sessionRows];
34556
- },
34557
- parse(text) {
34558
- return parseSessionReferenceText(text);
34559
- },
34560
- async prepare(parsed, signal) {
34561
- if (parsed.references.length === 0 || resolver === void 0 || agent === void 0) return {
34562
- text: parsed.text,
34563
- references: parsed.references
34564
- };
34565
- const prepared = await resolver.prepare(agent, [{
34566
- type: "text",
34567
- text: parsed.text
34568
- }], parsed.references, signal);
34569
- return {
34570
- text: prepared.content.filter((block) => block.type === "text").map((block) => block.text).join(""),
34571
- references: parsed.references,
34572
- additionalContext: prepared.additionalContext
34573
- };
34574
- },
34575
- sessionMention(candidate) {
34576
- return formatSessionReferenceMention({
34577
- sessionId: candidate.sessionId,
34578
- label: candidate.label
34579
- });
34580
- }
34581
- };
34582
- }
34583
- //#endregion
34584
34935
  //#region src/questions.ts
34585
34936
  const ABORT_ERROR = new UserQuestionError("ask_user_question was interrupted before the user answered", "ASK_ABORTED");
34586
34937
  /**
@@ -34681,7 +35032,10 @@ const NOTIFY_FRAME_MS$1 = 16;
34681
35032
  * @returns the store the runner feeds and the renderer subscribes to.
34682
35033
  */
34683
35034
  function createTranscriptStore(replay) {
34684
- let view = replay === void 0 ? createTranscriptView() : projectEvents(replay);
35035
+ let acc = createReplayAccumulator();
35036
+ for (const event of replay ?? []) replayProjectEvent(acc, event);
35037
+ let view = snapshotReplayView(acc);
35038
+ let dirty = false;
34685
35039
  const listeners = /* @__PURE__ */ new Set();
34686
35040
  let scheduled = false;
34687
35041
  let lastNotifyAt = 0;
@@ -34698,7 +35052,13 @@ function createTranscriptStore(replay) {
34698
35052
  else setTimeout(dispatch, wait);
34699
35053
  };
34700
35054
  return {
34701
- getView: () => view,
35055
+ getView: () => {
35056
+ if (dirty) {
35057
+ view = snapshotReplayView(acc);
35058
+ dirty = false;
35059
+ }
35060
+ return view;
35061
+ },
34702
35062
  subscribe(listener) {
34703
35063
  listeners.add(listener);
34704
35064
  return () => {
@@ -34706,13 +35066,13 @@ function createTranscriptStore(replay) {
34706
35066
  };
34707
35067
  },
34708
35068
  apply(event) {
34709
- const next = projectEvent(view, event);
34710
- if (next === view) return;
34711
- view = next;
35069
+ if (!replayProjectEvent(acc, event)) return;
35070
+ dirty = true;
34712
35071
  notify();
34713
35072
  },
34714
35073
  reset() {
34715
- view = createTranscriptView();
35074
+ acc = createReplayAccumulator();
35075
+ dirty = true;
34716
35076
  notify();
34717
35077
  }
34718
35078
  };
@@ -34830,6 +35190,7 @@ function foldSubagentRow(previous, sessionId, event) {
34830
35190
  */
34831
35191
  function createSubagentFeed() {
34832
35192
  let rows = Object.freeze([]);
35193
+ const seen = /* @__PURE__ */ new Set();
34833
35194
  const listeners = /* @__PURE__ */ new Set();
34834
35195
  let scheduled = false;
34835
35196
  let lastNotifyAt = 0;
@@ -34850,12 +35211,30 @@ function createSubagentFeed() {
34850
35211
  const index = rows.findIndex((row) => row.id === sessionId);
34851
35212
  const previous = index === -1 ? void 0 : rows[index];
34852
35213
  const next = foldSubagentRow(previous, sessionId, event);
34853
- if (next === previous) return;
34854
- if (index === -1 && rows.length >= 8) return;
34855
- rows = Object.freeze(index === -1 ? [...rows, next] : rows.map((row, at) => at === index ? next : row));
35214
+ if (index !== -1) {
35215
+ if (next === previous) return;
35216
+ rows = Object.freeze(rows.map((row, at) => at === index ? next : row));
35217
+ notify();
35218
+ return;
35219
+ }
35220
+ const counted = !seen.has(sessionId);
35221
+ if (counted) seen.add(sessionId);
35222
+ if (rows.length >= 8) {
35223
+ const evict = rows.findIndex((row) => row.state === "done");
35224
+ if (evict === -1) {
35225
+ if (counted) notify();
35226
+ return;
35227
+ }
35228
+ rows = Object.freeze([
35229
+ ...rows.slice(0, evict),
35230
+ next,
35231
+ ...rows.slice(evict + 1)
35232
+ ]);
35233
+ } else rows = Object.freeze([...rows, next]);
34856
35234
  notify();
34857
35235
  },
34858
35236
  reset() {
35237
+ seen.clear();
34859
35238
  if (rows.length === 0) return;
34860
35239
  rows = Object.freeze([]);
34861
35240
  notify();
@@ -34868,6 +35247,9 @@ function createSubagentFeed() {
34868
35247
  },
34869
35248
  getSnapshot() {
34870
35249
  return rows;
35250
+ },
35251
+ getTotalSeen() {
35252
+ return seen.size;
34871
35253
  }
34872
35254
  };
34873
35255
  }
@@ -35057,6 +35439,312 @@ function latestAssistantText(view) {
35057
35439
  }
35058
35440
  }
35059
35441
  //#endregion
35442
+ //#region src/editor-keys.ts
35443
+ /**
35444
+ * VS Code-family terminal keybinding repair. VS Code hands Ctrl+R to the
35445
+ * workbench (Open Recent) even while an integrated terminal owns focus, so
35446
+ * the reasoning-fold key never reaches the TUI. Workspace-scoped keybindings
35447
+ * do not exist, so the fix is one user-level keybindings.json rule that
35448
+ * forwards the raw Ctrl byte via sendSequence under terminalFocus. This
35449
+ * module detects the hosting editor variant, resolves its user
35450
+ * keybindings.json, and merges the rule idempotently; pure merge/detect
35451
+ * helpers are separated from the fs orchestration so both stay testable.
35452
+ * @module @deepseek-ai/dsh-code/editor-keys
35453
+ */
35454
+ /** Install directory names per family ('vscode' covers stable and Insiders). */
35455
+ const FAMILY_DIRS = {
35456
+ vscode: ["Code", "Code - Insiders"],
35457
+ cursor: ["Cursor"],
35458
+ vscodium: ["VSCodium"],
35459
+ windsurf: ["Windsurf"]
35460
+ };
35461
+ /**
35462
+ * Detect the editor hosting this integrated terminal.
35463
+ * @param env - process environment (TERM_PROGRAM decides; case/whitespace tolerant).
35464
+ * @returns the family, or undefined outside VS Code-family terminals.
35465
+ */
35466
+ function detectEditorTerminalFamily(env = process.env) {
35467
+ const program = env.TERM_PROGRAM?.trim().toLowerCase();
35468
+ if (program === "vscode" || program === "cursor" || program === "vscodium" || program === "windsurf") return program;
35469
+ }
35470
+ /**
35471
+ * Whether the pty is hosted away from the editor UI (ssh/container/tunnel).
35472
+ * Keybindings live on the client machine, so a remote session must never
35473
+ * write them server-side.
35474
+ */
35475
+ function isRemoteTerminalEnv(env = process.env) {
35476
+ return (env.VSCODE_IPC_HOOK_CLI ?? "").trim() !== "";
35477
+ }
35478
+ /**
35479
+ * Resolve the user keybindings.json candidates for one family, most likely
35480
+ * install first. Only paths that exist on disk are repaired.
35481
+ */
35482
+ function editorKeybindingCandidates(family, context) {
35483
+ return FAMILY_DIRS[family].map((dir) => {
35484
+ if (context.platform === "win32") return join(context.appdata ?? join(context.homedir, "AppData", "Roaming"), dir, "User", "keybindings.json");
35485
+ if (context.platform === "darwin") return join(context.homedir, "Library", "Application Support", dir, "User", "keybindings.json");
35486
+ return join(context.homedir, ".config", dir, "User", "keybindings.json");
35487
+ });
35488
+ }
35489
+ /** The one workbench rule that hands Ctrl+R to the focused terminal. */
35490
+ const CTRL_R_PASSTHROUGH_RULE = {
35491
+ key: "ctrl+r",
35492
+ command: "workbench.action.terminal.sendSequence",
35493
+ args: { text: "" },
35494
+ when: "terminalFocus"
35495
+ };
35496
+ /** Serialized rule block (4-space indent, the editors' default style). */
35497
+ const RULE_BLOCK = [
35498
+ " {",
35499
+ " \"key\": \"ctrl+r\",",
35500
+ " \"command\": \"workbench.action.terminal.sendSequence\",",
35501
+ " \"args\": { \"text\": \"\\u0012\" },",
35502
+ " \"when\": \"terminalFocus\"",
35503
+ " }"
35504
+ ].join("\n");
35505
+ /** Fresh-file template carrying the editors' standard header comment. */
35506
+ const KEYBINDINGS_TEMPLATE = "// Place your key bindings in this file to override the defaults\n[\n" + RULE_BLOCK + "\n]\n";
35507
+ /** Whether one parsed keybindings entry already forwards Ctrl+R to the terminal. */
35508
+ function isCtrlRPassthroughEntry(entry) {
35509
+ if (typeof entry !== "object" || entry === null) return false;
35510
+ const record = entry;
35511
+ if (record.command !== CTRL_R_PASSTHROUGH_RULE.command) return false;
35512
+ if (typeof record.key !== "string" || record.key.trim().toLowerCase() !== CTRL_R_PASSTHROUGH_RULE.key) return false;
35513
+ const args = record.args;
35514
+ if (typeof args !== "object" || args === null) return false;
35515
+ const text = args.text;
35516
+ if (typeof text !== "string" || !text.includes("")) return false;
35517
+ return typeof record.when === "string" && /\bterminalFocus\b/u.test(record.when);
35518
+ }
35519
+ /**
35520
+ * Remove // and block comments from one JSONC document. Double-quoted strings
35521
+ * survive untouched, so comment markers inside string values are preserved.
35522
+ */
35523
+ function stripJsoncComments(text) {
35524
+ let out = "";
35525
+ let index = 0;
35526
+ let inString = false;
35527
+ while (index < text.length) {
35528
+ const char = text[index];
35529
+ if (inString) {
35530
+ out += char;
35531
+ if (char === "\\" && index + 1 < text.length) {
35532
+ out += text[index + 1];
35533
+ index += 2;
35534
+ continue;
35535
+ }
35536
+ if (char === "\"") inString = false;
35537
+ index += 1;
35538
+ continue;
35539
+ }
35540
+ if (char === "\"") {
35541
+ inString = true;
35542
+ out += char;
35543
+ index += 1;
35544
+ continue;
35545
+ }
35546
+ if (char === "/" && text[index + 1] === "/") {
35547
+ while (index < text.length && text[index] !== "\n") index += 1;
35548
+ continue;
35549
+ }
35550
+ if (char === "/" && text[index + 1] === "*") {
35551
+ index += 2;
35552
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
35553
+ if (text[index] === "\n") out += "\n";
35554
+ index += 1;
35555
+ }
35556
+ index += 2;
35557
+ continue;
35558
+ }
35559
+ out += char;
35560
+ index += 1;
35561
+ }
35562
+ return out;
35563
+ }
35564
+ /** Drop commas directly before a closing bracket (string-aware). */
35565
+ function removeTrailingCommas(text) {
35566
+ let out = "";
35567
+ let index = 0;
35568
+ let inString = false;
35569
+ while (index < text.length) {
35570
+ const char = text[index];
35571
+ if (inString) {
35572
+ out += char;
35573
+ if (char === "\\" && index + 1 < text.length) {
35574
+ out += text[index + 1];
35575
+ index += 2;
35576
+ continue;
35577
+ }
35578
+ if (char === "\"") inString = false;
35579
+ index += 1;
35580
+ continue;
35581
+ }
35582
+ if (char === "\"") {
35583
+ inString = true;
35584
+ out += char;
35585
+ index += 1;
35586
+ continue;
35587
+ }
35588
+ if (char === ",") {
35589
+ let peek = index + 1;
35590
+ while (peek < text.length && (text[peek] === " " || text[peek] === " " || text[peek] === "\n" || text[peek] === "\r")) peek += 1;
35591
+ const next = text[peek];
35592
+ if (next === "}" || next === "]") {
35593
+ index += 1;
35594
+ continue;
35595
+ }
35596
+ }
35597
+ out += char;
35598
+ index += 1;
35599
+ }
35600
+ return out;
35601
+ }
35602
+ /** Parse one JSONC document; trailing commas are tolerated. */
35603
+ function parseJsonc(text) {
35604
+ return JSON.parse(removeTrailingCommas(stripJsoncComments(text)));
35605
+ }
35606
+ /** Raw index of the top-level rule array's opening bracket, or -1 when absent. */
35607
+ function rawOpenBracketIndex(text) {
35608
+ let index = 0;
35609
+ let inString = false;
35610
+ while (index < text.length) {
35611
+ const char = text[index];
35612
+ if (inString) {
35613
+ if (char === "\\") {
35614
+ index += 2;
35615
+ continue;
35616
+ }
35617
+ if (char === "\"") inString = false;
35618
+ index += 1;
35619
+ continue;
35620
+ }
35621
+ if (char === "\"") {
35622
+ inString = true;
35623
+ index += 1;
35624
+ continue;
35625
+ }
35626
+ if (char === "/" && text[index + 1] === "/") {
35627
+ while (index < text.length && text[index] !== "\n") index += 1;
35628
+ continue;
35629
+ }
35630
+ if (char === "/" && text[index + 1] === "*") {
35631
+ index += 2;
35632
+ while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) index += 1;
35633
+ index += 2;
35634
+ continue;
35635
+ }
35636
+ if (char === "[") return index;
35637
+ index += 1;
35638
+ }
35639
+ return -1;
35640
+ }
35641
+ /**
35642
+ * Merge the Ctrl+R passthrough into one keybindings.json document. The raw
35643
+ * text is preserved verbatim (comments included); the rule is inserted right
35644
+ * after the array opener so it cannot be shadowed by later conflicting user
35645
+ * rules. Missing files resolve to a fresh template.
35646
+ * @throws when the document does not carry a rule array.
35647
+ */
35648
+ function mergeCtrlRPassthrough(raw) {
35649
+ if (raw === void 0) return {
35650
+ status: "created",
35651
+ text: KEYBINDINGS_TEMPLATE
35652
+ };
35653
+ const parsed = parseJsonc(raw);
35654
+ if (!Array.isArray(parsed)) throw new Error("keybindings.json does not contain a rule array");
35655
+ if (parsed.some(isCtrlRPassthroughEntry)) return { status: "present" };
35656
+ const open = rawOpenBracketIndex(raw);
35657
+ if (open === -1) throw new Error("keybindings.json does not contain a rule array");
35658
+ const insert = parsed.length > 0 ? "\n" + RULE_BLOCK + "," : "\n" + RULE_BLOCK;
35659
+ return {
35660
+ status: "updated",
35661
+ text: raw.slice(0, open + 1) + insert + raw.slice(open + 1)
35662
+ };
35663
+ }
35664
+ /** Parse one flag file snapshot; missing or corrupt content degrades to unshown. */
35665
+ function parseEditorKeysFlag(raw) {
35666
+ if (raw === void 0) return {};
35667
+ try {
35668
+ const parsed = JSON.parse(raw);
35669
+ if (typeof parsed !== "object" || parsed === null) return {};
35670
+ const shown = parsed.hintShownAt;
35671
+ return typeof shown === "string" ? { hintShownAt: shown } : {};
35672
+ } catch {
35673
+ return {};
35674
+ }
35675
+ }
35676
+ /** Persist the shown marker; best-effort, the hint is cosmetic and never a gate. */
35677
+ async function markEditorKeysHintShown(path) {
35678
+ await mkdir(dirname(path), { recursive: true });
35679
+ await writeFile(path, JSON.stringify({ hintShownAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n", "utf8");
35680
+ }
35681
+ /** Read one file if it exists; undefined otherwise (ENOENT and unreadable both). */
35682
+ async function readIfPresent(path) {
35683
+ try {
35684
+ return await readFile(path, "utf8");
35685
+ } catch {
35686
+ return;
35687
+ }
35688
+ }
35689
+ /**
35690
+ * Apply the Ctrl+R passthrough to every local keybindings.json of the hosting
35691
+ * editor and mark the startup hint shown. Existing files get a .dsh-bak
35692
+ * backup before the first write.
35693
+ * @returns a one-line user-facing summary.
35694
+ * @throws with an actionable message when the environment cannot be repaired.
35695
+ */
35696
+ async function applyCtrlRPassthrough({ env, paths, flagPath }) {
35697
+ const family = detectEditorTerminalFamily(env);
35698
+ if (family === void 0) throw new Error("not a VS Code-family terminal (TERM_PROGRAM=" + (env.TERM_PROGRAM?.trim() || "unset") + "); add the ctrl+r rule to keybindings.json manually");
35699
+ if (isRemoteTerminalEnv(env)) throw new Error("remote terminal detected; apply the keybindings rule on the local machine instead");
35700
+ const candidates = editorKeybindingCandidates(family, paths);
35701
+ const targets = [];
35702
+ for (const candidate of candidates) if (await readIfPresent(candidate) !== void 0) targets.push(candidate);
35703
+ if (targets.length === 0) targets.push(candidates[0]);
35704
+ const updated = [];
35705
+ const present = [];
35706
+ for (const target of targets) {
35707
+ const raw = await readIfPresent(target);
35708
+ const merge = mergeCtrlRPassthrough(raw);
35709
+ if (merge.status === "present") {
35710
+ present.push(target);
35711
+ continue;
35712
+ }
35713
+ await mkdir(dirname(target), { recursive: true });
35714
+ if (raw !== void 0) await writeFile(target + ".dsh-bak", raw, "utf8");
35715
+ await writeFile(target, merge.text, "utf8");
35716
+ updated.push(target);
35717
+ }
35718
+ await markEditorKeysHintShown(flagPath).catch(() => {});
35719
+ const label = (target) => basename(dirname(dirname(target)));
35720
+ if (updated.length === 0) return "ctrl+r passthrough already configured in " + present.map(label).join(", ");
35721
+ return "ctrl+r passthrough written to " + updated.map(label).join(", ") + " — effective immediately";
35722
+ }
35723
+ /**
35724
+ * Resolve the one-shot startup hint for VS Code-family terminals. Fires at
35725
+ * most once per install (flag file), never when the passthrough rule is
35726
+ * already present, and never in remote ptys where the repair cannot run.
35727
+ * @returns the hint line, or undefined to stay silent.
35728
+ */
35729
+ async function resolveEditorKeysStartupHint({ env, paths, flagPath }) {
35730
+ if (parseEditorKeysFlag(await readIfPresent(flagPath)).hintShownAt !== void 0) return void 0;
35731
+ const family = detectEditorTerminalFamily(env);
35732
+ if (family === void 0 || isRemoteTerminalEnv(env)) return void 0;
35733
+ for (const candidate of editorKeybindingCandidates(family, paths)) {
35734
+ const raw = await readIfPresent(candidate);
35735
+ if (raw === void 0) continue;
35736
+ try {
35737
+ const parsed = parseJsonc(raw);
35738
+ if (Array.isArray(parsed) && parsed.some(isCtrlRPassthroughEntry)) {
35739
+ await markEditorKeysHintShown(flagPath).catch(() => {});
35740
+ return;
35741
+ }
35742
+ } catch {}
35743
+ }
35744
+ await markEditorKeysHintShown(flagPath).catch(() => {});
35745
+ return "run /vscode-keys to pass ctrl+r through this editor (alt+r works meanwhile)";
35746
+ }
35747
+ //#endregion
35060
35748
  //#region src/fork.ts
35061
35749
  /** Select a completed turn and trailing between-turn metadata. */
35062
35750
  function selectForkSeed(events, atSeq) {
@@ -35123,13 +35811,14 @@ function parseGitDiffSpec(argument) {
35123
35811
  ]
35124
35812
  };
35125
35813
  }
35126
- function executeGit(cwd, args) {
35814
+ function executeGit(cwd, args, signal) {
35127
35815
  return new Promise((resolve, reject) => {
35128
35816
  execFile("git", [...args], {
35129
35817
  cwd,
35130
35818
  encoding: "utf8",
35131
35819
  maxBuffer: 16777216,
35132
- windowsHide: true
35820
+ windowsHide: true,
35821
+ signal
35133
35822
  }, (error, stdout, stderr) => {
35134
35823
  if (error !== null) {
35135
35824
  reject(new Error(stderr.trim() || error.message));
@@ -35139,11 +35828,15 @@ function executeGit(cwd, args) {
35139
35828
  });
35140
35829
  });
35141
35830
  }
35142
- /** Load one complete textual diff without invoking external diff drivers. */
35143
- async function loadGitDiff(cwd, argument) {
35831
+ /**
35832
+ * Load one complete textual diff without invoking external diff drivers.
35833
+ * @param signal - aborted by the caller on session switches/quit, killing the
35834
+ * git subprocess instead of letting a stale repository's diff land later.
35835
+ */
35836
+ async function loadGitDiff(cwd, argument, signal) {
35144
35837
  const spec = parseGitDiffSpec(argument);
35145
35838
  try {
35146
- const text = await executeGit(cwd, spec.args);
35839
+ const text = await executeGit(cwd, spec.args, signal);
35147
35840
  return {
35148
35841
  title: `git diff - ${spec.label}`,
35149
35842
  files: parseGitDiffFiles(text)
@@ -35157,7 +35850,7 @@ async function loadGitDiff(cwd, argument) {
35157
35850
  "--no-ext-diff",
35158
35851
  "--unified=3",
35159
35852
  "--"
35160
- ]))
35853
+ ], signal))
35161
35854
  };
35162
35855
  }
35163
35856
  }
@@ -35325,6 +36018,48 @@ function listPluginRows(ctx) {
35325
36018
  return rows;
35326
36019
  }
35327
36020
  //#endregion
36021
+ //#region src/settings-file.ts
36022
+ /**
36023
+ * Serialized, crash-atomic persistence for the small user-level JSON files
36024
+ * under the DSH home (statusline.json, theme.json). Two guarantees the bare
36025
+ * floating `writeFile` path could not give:
36026
+ *
36027
+ * 1. Every save is appended to ONE chain, so rapid consecutive edits land
36028
+ * in submission order and the last snapshot is the one on disk (parallel
36029
+ * floating writes let an older snapshot finish last and win).
36030
+ * 2. Each write goes to a sibling temp file first and is renamed into
36031
+ * place, so a crash mid-write can never leave a half-written JSON
36032
+ * document behind.
36033
+ *
36034
+ * The chain itself never rejects: a failed write is reported to that
36035
+ * save's caller while later saves keep their turn.
36036
+ *
36037
+ * @module @deepseek-ai/dsh-code/settings-file
36038
+ */
36039
+ /**
36040
+ * Create the shared settings-write chain. One instance per process keeps
36041
+ * every user-level JSON file mutually serialized.
36042
+ * @returns the persistence handle.
36043
+ */
36044
+ function createUserSettingsPersistence() {
36045
+ let chain = Promise.resolve();
36046
+ return {
36047
+ save(path, text) {
36048
+ const write = chain.then(async () => {
36049
+ await mkdir(dirname(path), { recursive: true });
36050
+ const temp = `${path}.tmp`;
36051
+ await writeFile(temp, text, "utf8");
36052
+ await rename(temp, path);
36053
+ });
36054
+ chain = write.catch(() => {});
36055
+ return write;
36056
+ },
36057
+ flush() {
36058
+ return chain;
36059
+ }
36060
+ };
36061
+ }
36062
+ //#endregion
35328
36063
  //#region src/index.ts
35329
36064
  /**
35330
36065
  * @deepseek-ai/dsh-code — the interactive terminal driver. The bundle patch
@@ -35648,6 +36383,20 @@ async function run(ctx, startup, io) {
35648
36383
  });
35649
36384
  const questions = mountQuestionProvider(ctx);
35650
36385
  const bridge = { notify: () => {} };
36386
+ const capabilitySyncDebounceMs = 400;
36387
+ const runCapabilitySync = () => {
36388
+ syncModelCapabilities(ctx, bridge.notify);
36389
+ };
36390
+ let capabilitySyncTimer;
36391
+ const scheduleCapabilitySync = () => {
36392
+ if (capabilitySyncTimer !== void 0) clearTimeout(capabilitySyncTimer);
36393
+ capabilitySyncTimer = setTimeout(() => {
36394
+ capabilitySyncTimer = void 0;
36395
+ runCapabilitySync();
36396
+ }, capabilitySyncDebounceMs);
36397
+ };
36398
+ const offCapabilitySync = [ctx.on("settings/document-updated", scheduleCapabilitySync), ctx.on("llm/adapters-updated", scheduleCapabilitySync)];
36399
+ scheduleCapabilitySync();
35651
36400
  const statuslinePath = join(homedir(), ".dsh", "dsh-code", "statusline.json");
35652
36401
  let statuslineWarning;
35653
36402
  let statuslineItems = [];
@@ -35657,12 +36406,23 @@ async function run(ctx, startup, io) {
35657
36406
  statuslineItems = parseStatuslineItems(void 0);
35658
36407
  if (error.code !== "ENOENT") statuslineWarning = error instanceof Error ? error.message : String(error);
35659
36408
  }
36409
+ const settingsPersistence = createUserSettingsPersistence();
35660
36410
  const saveStatusline = (items) => {
35661
36411
  statuslineItems = [...items];
35662
- mkdir(dirname(statuslinePath), { recursive: true }).then(() => writeFile(statuslinePath, JSON.stringify({ items }, null, 2) + "\n", "utf8")).catch((writeError) => {
36412
+ settingsPersistence.save(statuslinePath, JSON.stringify({ items }, null, 2) + "\n").catch((writeError) => {
35663
36413
  bridge.notify("statusline save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
35664
36414
  });
35665
36415
  };
36416
+ const editorKeysEnv = {
36417
+ env: process.env,
36418
+ paths: {
36419
+ homedir: homedir(),
36420
+ appdata: process.env.APPDATA,
36421
+ platform: process.platform
36422
+ },
36423
+ flagPath: join(homedir(), ".dsh", "dsh-code", "editor-keys.json")
36424
+ };
36425
+ const applyEditorKeys = () => applyCtrlRPassthrough(editorKeysEnv);
35666
36426
  const themePath = join(homedir(), ".dsh", "dsh-code", "theme.json");
35667
36427
  let themeWarning;
35668
36428
  if (startup.theme === void 0) try {
@@ -35673,7 +36433,7 @@ async function run(ctx, startup, io) {
35673
36433
  else setTheme(startup.theme);
35674
36434
  const saveTheme = (name) => {
35675
36435
  setTheme(name);
35676
- mkdir(dirname(themePath), { recursive: true }).then(() => writeFile(themePath, JSON.stringify({ theme: name }, null, 2) + "\n", "utf8")).catch((writeError) => {
36436
+ settingsPersistence.save(themePath, JSON.stringify({ theme: name }, null, 2) + "\n").catch((writeError) => {
35677
36437
  bridge.notify("theme save failed: " + (writeError instanceof Error ? writeError.message : String(writeError)), "error");
35678
36438
  });
35679
36439
  };
@@ -35712,6 +36472,8 @@ async function run(ctx, startup, io) {
35712
36472
  quitAbort.abort();
35713
36473
  epoch += 1;
35714
36474
  off();
36475
+ for (const dispose of offCapabilitySync) dispose();
36476
+ if (capabilitySyncTimer !== void 0) clearTimeout(capabilitySyncTimer);
35715
36477
  mountRef.current?.unmount();
35716
36478
  const currentSession = session;
35717
36479
  const currentActive = active;
@@ -35735,6 +36497,10 @@ async function run(ctx, startup, io) {
35735
36497
  {
35736
36498
  name: "history",
35737
36499
  run: () => historyWriteChain
36500
+ },
36501
+ {
36502
+ name: "settings",
36503
+ run: () => settingsPersistence.flush()
35738
36504
  }
35739
36505
  ], io.exit, report);
35740
36506
  };
@@ -36054,10 +36820,21 @@ async function run(ctx, startup, io) {
36054
36820
  * Delete one session subtree (/delete, codex semantics: subagent threads go
36055
36821
  * with their root). The kernel persistence seam has NO deletion API by
36056
36822
  * design — logs accumulate "until removed externally" — so this is the
36057
- * controlled external removal: guards (live/current refusal, subtree
36058
- * collection, and the JSONL layout check `encodeSegment(id)/session.jsonl`)
36059
- * run before any filesystem touch, and only the backend-located artifacts
36060
- * are removed. Backends without a locatable artifact (SQLite) are refused.
36823
+ * controlled external removal, in three phases with a hard boundary
36824
+ * between planning and touching the filesystem:
36825
+ *
36826
+ * 1. `planSessionDeletion` collects the subtree and refuses when the root
36827
+ * or ANY member is live (a live child would outlive its deleted
36828
+ * parent), ordering the plan children-first.
36829
+ * 2. Every plan node must locate to a guarded artifact directory
36830
+ * (`encodeSegment(id)`/`session.jsonl` layout). Backends without a
36831
+ * locatable artifact (SQLite) refuse the WHOLE deletion here — no
36832
+ * file has been touched yet, so a backend or layout surprise can
36833
+ * never strand a half-deleted subtree.
36834
+ * 3. Artifacts are removed children-first: only an I/O error mid-delete
36835
+ * can stop it short (reported with removed/total counts), leaving the
36836
+ * shallowest lineage intact.
36837
+ *
36061
36838
  * @param id - the root session id to delete.
36062
36839
  * @returns the outcome line for the panel/notice.
36063
36840
  */
@@ -36065,19 +36842,22 @@ async function run(ctx, startup, io) {
36065
36842
  if (sessionQuery === void 0) return "session query is unavailable in this profile";
36066
36843
  if (session !== void 0 && session.id === id) return "cannot delete the session you are using — switch or /new first";
36067
36844
  const records = await sessionQuery.listSessions();
36068
- const target = records.find((record) => record.header.id === id);
36069
- if (target === void 0) return `no persisted session matches "${id}"`;
36070
- if (target.live) return "cannot delete a live session — it is open in this or another process";
36071
- const doomed = collectDeletionSubtree(records, id);
36845
+ const plan = planSessionDeletion(records, id);
36846
+ if (!plan.ok) return plan.reason;
36072
36847
  const byId = new Map(records.map((record) => [record.header.id, record]));
36073
- let removed = 0;
36074
- for (const candidate of doomed) {
36075
- const record = byId.get(candidate);
36076
- if (record === void 0 || record.live) continue;
36848
+ const dirs = /* @__PURE__ */ new Map();
36849
+ for (const node of plan.nodes) {
36850
+ const record = byId.get(node.id);
36851
+ if (record === void 0) return `no persisted session matches "${node.id}"`;
36077
36852
  const location = persistence?.locate(record.header);
36078
- if (location === void 0) return `session backend exposes no deletable artifact for ${candidate.slice(-12)} (deletion is unsupported on this backend)`;
36079
- const dir = sessionArtifactDirectory(location.path, candidate);
36853
+ if (location === void 0) return `session backend exposes no deletable artifact for ${node.id.slice(-12)} (deletion is unsupported on this backend)`;
36854
+ const dir = sessionArtifactDirectory(location.path, node.id);
36080
36855
  if (dir === void 0) return `refusing to delete: unexpected artifact layout at ${location.path}`;
36856
+ dirs.set(node.id, dir);
36857
+ }
36858
+ let removed = 0;
36859
+ for (const node of plan.nodes) {
36860
+ const dir = dirs.get(node.id);
36081
36861
  try {
36082
36862
  for (const name of SESSION_ARTIFACT_NAMES) await rm(join(dir, name), { force: true });
36083
36863
  await rm(dir, {
@@ -36086,7 +36866,7 @@ async function run(ctx, startup, io) {
36086
36866
  }).catch(() => {});
36087
36867
  removed += 1;
36088
36868
  } catch (error) {
36089
- return `delete failed for ${candidate.slice(-12)}: ${error instanceof Error ? error.message : String(error)}`;
36869
+ return `delete failed for ${node.id.slice(-12)} after ${removed} of ${plan.nodes.length}: ${error instanceof Error ? error.message : String(error)}`;
36090
36870
  }
36091
36871
  }
36092
36872
  return `deleted ${removed} session${removed === 1 ? "" : "s"}`;
@@ -36111,13 +36891,20 @@ async function run(ctx, startup, io) {
36111
36891
  pendingModeWork = choice.then(() => {}, () => {});
36112
36892
  return choice;
36113
36893
  }
36114
- const preset = await selectPreset(presets, currentAgent, id);
36115
- if (active === void 0) throw new Error("active Agent has no session state");
36116
- active.mode = preset.id;
36117
- commands.setAgent(currentAgent);
36118
- skills.setAgent(currentAgent);
36119
- renderCurrent();
36120
- return preset.id;
36894
+ const currentActive = active;
36895
+ const atEpoch = epoch;
36896
+ let selected;
36897
+ await compose(async () => {
36898
+ const preset = await selectPreset(presets, currentAgent, id);
36899
+ if (epoch !== atEpoch || agent !== currentAgent || active !== currentActive) throw new Error("session changed while switching mode — nothing applied; retry in the active session");
36900
+ if (active === void 0) throw new Error("active Agent has no session state");
36901
+ active.mode = preset.id;
36902
+ commands.setAgent(currentAgent);
36903
+ skills.setAgent(currentAgent);
36904
+ selected = preset.id;
36905
+ renderCurrent();
36906
+ });
36907
+ return selected;
36121
36908
  };
36122
36909
  const activate = (nextTarget) => {
36123
36910
  if (quitting) return Promise.resolve();
@@ -36231,7 +37018,21 @@ async function run(ctx, startup, io) {
36231
37018
  });
36232
37019
  };
36233
37020
  const reviewChanges = (argument) => {
36234
- loadGitDiff(session?.header.cwd ?? cwd, argument).then(({ title, files }) => {
37021
+ const currentAgent = agent;
37022
+ if (currentAgent === void 0) {
37023
+ bridge.notify("no session yet - submit a message to start", "warning");
37024
+ return;
37025
+ }
37026
+ const atEpoch = epoch;
37027
+ const reviewCwd = session?.header.cwd ?? cwd;
37028
+ const controller = new AbortController();
37029
+ pendingControllers.add(controller);
37030
+ const finish = () => {
37031
+ pendingControllers.delete(controller);
37032
+ };
37033
+ loadGitDiff(reviewCwd, argument, controller.signal).then(({ title, files }) => {
37034
+ finish();
37035
+ if (controller.signal.aborted || epoch !== atEpoch || agent !== currentAgent) return;
36235
37036
  try {
36236
37037
  setPermissionAction("read-only");
36237
37038
  } catch (error) {
@@ -36241,6 +37042,8 @@ async function run(ctx, startup, io) {
36241
37042
  send(buildReviewPrompt(files.flatMap((file) => file.lines).join("\n"), title), "followup");
36242
37043
  bridge.notify("review started under read-only permissions");
36243
37044
  }, (error) => {
37045
+ finish();
37046
+ if (controller.signal.aborted || epoch !== atEpoch) return;
36244
37047
  bridge.notify(`review failed: ${error instanceof Error ? error.message : String(error)}`, "error");
36245
37048
  });
36246
37049
  };
@@ -36331,7 +37134,7 @@ async function run(ctx, startup, io) {
36331
37134
  copyTextValue: copyText,
36332
37135
  loadMentions: (query, signal) => mentions.candidates(query, signal),
36333
37136
  inspectImages: (paths) => inspectImagePaths(paths, ctx.get("attachments"), session?.header.cwd ?? cwd),
36334
- prepareImages: (paths) => saveImagePaths(paths, ctx.get("attachments")),
37137
+ prepareImages: (paths, signal) => saveImagePaths(paths, ctx.get("attachments"), signal),
36335
37138
  cyclePermission: cyclePermission$1,
36336
37139
  setPermission: setPermissionAction,
36337
37140
  selectModel,
@@ -36368,6 +37171,7 @@ async function run(ctx, startup, io) {
36368
37171
  loadJobs: () => listJobs(ctx, active?.agent),
36369
37172
  statusline: statuslineItems,
36370
37173
  saveStatusline,
37174
+ applyEditorKeys,
36371
37175
  saveTheme,
36372
37176
  history: inputHistory,
36373
37177
  recordHistory,
@@ -36400,6 +37204,12 @@ async function run(ctx, startup, io) {
36400
37204
  if (themeWarning !== void 0) setTimeout(() => {
36401
37205
  bridge.notify("theme config unreadable, using dark: " + themeWarning, "warning");
36402
37206
  }, 50);
37207
+ resolveEditorKeysStartupHint(editorKeysEnv).then((hint) => {
37208
+ if (hint === void 0) return;
37209
+ setTimeout(() => {
37210
+ bridge.notify(hint);
37211
+ }, 50);
37212
+ }, () => {});
36403
37213
  }
36404
37214
  /**
36405
37215
  * Mount the interactive terminal driver.