dsh-code 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.en.md +31 -8
  2. package/README.md +264 -241
  3. package/bin/deepseek.mjs +100 -6
  4. package/cordis.patch.yml +29 -1
  5. package/lib/index.mjs +2317 -708
  6. package/lib/startup.mjs +21 -11
  7. package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
  8. package/lib/types/app.d.ts +66 -14
  9. package/lib/types/attachments.d.ts +7 -0
  10. package/lib/types/editor.d.ts +6 -0
  11. package/lib/types/fork.d.ts +8 -0
  12. package/lib/types/git-workflow.d.ts +23 -0
  13. package/lib/types/index.d.ts +6 -0
  14. package/lib/types/kernel-panels.d.ts +39 -0
  15. package/lib/types/keyboard.d.ts +43 -0
  16. package/lib/types/mentions.d.ts +28 -38
  17. package/lib/types/presets.d.ts +1 -3
  18. package/lib/types/provider-settings.d.ts +16 -0
  19. package/lib/types/render/animations.d.ts +24 -41
  20. package/lib/types/render/editor.d.ts +137 -0
  21. package/lib/types/render/export.d.ts +1 -1
  22. package/lib/types/render/lines.d.ts +6 -2
  23. package/lib/types/render/markdown.d.ts +3 -1
  24. package/lib/types/render/projection.d.ts +29 -3
  25. package/lib/types/render/status.d.ts +5 -12
  26. package/lib/types/session-directory.d.ts +1 -3
  27. package/lib/types/startup.d.ts +14 -11
  28. package/lib/types/store.d.ts +11 -9
  29. package/lib/types/subagents.d.ts +3 -3
  30. package/lib/types/theme.d.ts +14 -1
  31. package/lib/types/version.d.ts +15 -2
  32. package/package.json +153 -117
  33. package/src/app.ts +4455 -3904
  34. package/src/attachments.ts +44 -0
  35. package/src/editor.ts +51 -0
  36. package/src/fork.ts +31 -0
  37. package/src/git-workflow.ts +87 -0
  38. package/src/index.ts +1510 -1374
  39. package/src/internals.ts +14 -1
  40. package/src/kernel-panels.ts +914 -798
  41. package/src/keyboard.ts +125 -0
  42. package/src/mentions.ts +72 -117
  43. package/src/presets.ts +1 -4
  44. package/src/provider-settings.ts +94 -0
  45. package/src/render/animations.ts +74 -60
  46. package/src/render/editor.ts +398 -0
  47. package/src/render/export.ts +79 -79
  48. package/src/render/lines.ts +342 -236
  49. package/src/render/markdown.ts +99 -26
  50. package/src/render/projection.ts +102 -19
  51. package/src/render/status.ts +713 -650
  52. package/src/render/text.ts +150 -150
  53. package/src/render/tool-detail.ts +3 -1
  54. package/src/session-directory.ts +3 -3
  55. package/src/startup.ts +136 -119
  56. package/src/store.ts +23 -11
  57. package/src/subagents.ts +13 -5
  58. package/src/theme.ts +214 -206
  59. package/src/version.ts +58 -1
package/lib/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-CMFfr-1z.mjs";
2
- import { a as getPalette, c as parseThemeName, i as error, l as setTheme, n as brand, o as getTheme, r as dim, s as inkColor, u as chalk } from "./theme-BEi4i_aN.mjs";
2
+ import { a as inkColor, c as chalk, i as getTheme, n as dim, o as parseThemeName, r as getPalette, s as setTheme } from "./theme-DCT8Y2xf.mjs";
3
3
  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, readdir, rm, stat, writeFile } from "node:fs/promises";
7
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { basename, dirname, join, resolve } from "node:path";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
@@ -14,9 +14,11 @@ 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";
17
18
  import { formatSessionReferenceMention, parseSessionReferenceText } from "@deepseek-ai/dsh-session-reference";
18
19
  import { UserQuestionError } from "@deepseek-ai/dsh-user-questions";
19
20
  import { isUserInvocable } from "@deepseek-ai/dsh-skill";
21
+ import { execFile, spawn } from "node:child_process";
20
22
  //#region node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
21
23
  /**
22
24
  * @license React
@@ -21652,7 +21654,7 @@ var import_emoji_regex = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(
21652
21654
  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;
21653
21655
  };
21654
21656
  })))(), 1);
21655
- const segmenter = new Intl.Segmenter();
21657
+ const segmenter$1 = new Intl.Segmenter();
21656
21658
  const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
21657
21659
  function stringWidth(string, options = {}) {
21658
21660
  if (typeof string !== "string" || string.length === 0) return 0;
@@ -21661,7 +21663,7 @@ function stringWidth(string, options = {}) {
21661
21663
  if (string.length === 0) return 0;
21662
21664
  let width = 0;
21663
21665
  const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
21664
- for (const { segment: character } of segmenter.segment(string)) {
21666
+ for (const { segment: character } of segmenter$1.segment(string)) {
21665
21667
  const codePoint = character.codePointAt(0);
21666
21668
  if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) continue;
21667
21669
  if (codePoint >= 8203 && codePoint <= 8207 || codePoint === 65279) continue;
@@ -24580,6 +24582,43 @@ function readPackageVersion(manifest = new URL("../package.json", import.meta.ur
24580
24582
  }
24581
24583
  /** Version of the installed dsh-code package. */
24582
24584
  const DSH_CODE_VERSION = readPackageVersion();
24585
+ /** The harness host package: the dsh CLI whose process runs the TUI plugin. */
24586
+ const DSH_HOST_PACKAGE_NAME = "@deepseek-ai/dsh";
24587
+ /** Parent levels above the host entry allowed to hold its package manifest. */
24588
+ const DSH_HOST_WALK_LIMIT = 4;
24589
+ /**
24590
+ * Resolve the running dsh CLI host's version from its entry file
24591
+ * (`process.argv[1]`, e.g. `.../@deepseek-ai/dsh/lib/bin.js`). Only a manifest
24592
+ * literally named `@deepseek-ai/dsh` counts, so an unrelated entry (vitest, a
24593
+ * plain node script) resolves to undefined instead of faking a kernel version.
24594
+ */
24595
+ function resolveDshHostVersion(entry = process.argv[1]) {
24596
+ if (entry === void 0 || entry === "") return void 0;
24597
+ let directory = dirname(resolve(entry));
24598
+ for (let depth = 0; depth < DSH_HOST_WALK_LIMIT; depth += 1) {
24599
+ try {
24600
+ const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
24601
+ if (parsed.name === DSH_HOST_PACKAGE_NAME && typeof parsed.version === "string" && parsed.version.length > 0) return parsed.version;
24602
+ } catch {}
24603
+ const parent = dirname(directory);
24604
+ if (parent === directory) return void 0;
24605
+ directory = parent;
24606
+ }
24607
+ }
24608
+ let cachedDshKernelVersion;
24609
+ let dshKernelVersionResolved = false;
24610
+ /**
24611
+ * The dsh kernel version the TUI runs on, memoized after the first probe: the
24612
+ * host process never changes within a run, and the header reads this on every
24613
+ * Static replay.
24614
+ */
24615
+ function dshKernelVersion() {
24616
+ if (!dshKernelVersionResolved) {
24617
+ cachedDshKernelVersion = resolveDshHostVersion();
24618
+ dshKernelVersionResolved = true;
24619
+ }
24620
+ return cachedDshKernelVersion;
24621
+ }
24583
24622
  //#endregion
24584
24623
  //#region src/render/tool-preview.ts
24585
24624
  /**
@@ -24682,7 +24721,7 @@ const MAX_DIFFS = 8;
24682
24721
  const MAX_DIFF_TEXT_CHARS = 24e3;
24683
24722
  /** Truncate one line to the visible-column budget with an ellipsis marker. */
24684
24723
  function clipLine(text) {
24685
- return text.length > MAX_LINE_COLUMNS ? `${text.slice(0, 239)}…` : text;
24724
+ return truncateColumns(displayText(text), MAX_LINE_COLUMNS);
24686
24725
  }
24687
24726
  /** Split text into lines, dropping the trailing empty element of a final newline. */
24688
24727
  function toLines(text) {
@@ -24871,6 +24910,23 @@ function appendStreamingTail(current, delta) {
24871
24910
  function textOf(content) {
24872
24911
  return content.filter((block) => block.type === "text").map((block) => block.text).join("");
24873
24912
  }
24913
+ /** Durable image references in their model-visible order. */
24914
+ function imagesOf(content) {
24915
+ return content.filter((block) => block.type === "image").map((block) => block.attachment);
24916
+ }
24917
+ /** Human-readable bounded image labels for transcript, inspector, and export surfaces. */
24918
+ function imageLabels(images) {
24919
+ if (images === void 0 || images.length === 0) return "";
24920
+ return images.map((image, index) => {
24921
+ const rawName = image.name?.trim() || `image ${index + 1}`;
24922
+ return `[image: ${rawName.length <= 80 ? rawName : `${rawName.slice(0, 79)}…`} · ${image.width}×${image.height} · ${image.bytes} B]`;
24923
+ }).join("\n");
24924
+ }
24925
+ /** Prompt text with its durable image labels, without exposing local paths or bytes. */
24926
+ function promptDisplayText(entry) {
24927
+ const labels = imageLabels(entry.images);
24928
+ return entry.text === "" ? labels : labels === "" ? entry.text : `${entry.text}\n${labels}`;
24929
+ }
24874
24930
  /** Join the reasoning blocks of a content list; non-reasoning blocks contribute nothing. */
24875
24931
  function reasoningOf(content) {
24876
24932
  return content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
@@ -24897,6 +24953,7 @@ function createTranscriptView() {
24897
24953
  streaming: "",
24898
24954
  streamingReasoning: "",
24899
24955
  todos: [],
24956
+ toolCallOrdinal: 0,
24900
24957
  busy: false,
24901
24958
  busySince: 0,
24902
24959
  model: "",
@@ -24972,13 +25029,15 @@ function projectEvent(view, event) {
24972
25029
  entries = entries.filter((entry) => !(entry.kind === "pending" && entry.messageId === message.id));
24973
25030
  }
24974
25031
  const text = textOf(message.content);
25032
+ const images = imagesOf(message.content);
24975
25033
  if (message.source.kind === "user") return {
24976
25034
  ...view,
24977
25035
  pending,
24978
25036
  entries: [...entries, {
24979
25037
  kind: "user",
24980
25038
  text,
24981
- notice: false
25039
+ notice: false,
25040
+ ...images.length === 0 ? {} : { images }
24982
25041
  }],
24983
25042
  stats: {
24984
25043
  ...view.stats,
@@ -25025,7 +25084,8 @@ function projectEvent(view, event) {
25025
25084
  kind: "pending",
25026
25085
  messageId: message.id,
25027
25086
  target,
25028
- text: pendingText(message.content)
25087
+ text: pendingText(message.content),
25088
+ ...imagesOf(message.content).length === 0 ? {} : { images: imagesOf(message.content) }
25029
25089
  }];
25030
25090
  return {
25031
25091
  ...view,
@@ -25080,7 +25140,8 @@ function projectEvent(view, event) {
25080
25140
  entries: [...view.entries, {
25081
25141
  kind: "assistant",
25082
25142
  text,
25083
- reasoning
25143
+ reasoning,
25144
+ interrupted: event.data.interrupted === true ? true : void 0
25084
25145
  }],
25085
25146
  stats: {
25086
25147
  ...view.stats,
@@ -25107,11 +25168,14 @@ function projectEvent(view, event) {
25107
25168
  const turnTools = view.anchors.turnTools.get(data.turn) ?? /* @__PURE__ */ new Set();
25108
25169
  turnTools.add(data.callId);
25109
25170
  view.anchors.turnTools.set(data.turn, turnTools);
25171
+ const ordinal = view.toolCallOrdinal + 1;
25110
25172
  return {
25111
25173
  ...view,
25174
+ toolCallOrdinal: ordinal,
25112
25175
  entries: [...view.entries, {
25113
25176
  kind: "tool",
25114
25177
  callId: data.callId,
25178
+ ordinal,
25115
25179
  name: data.name,
25116
25180
  arguments: data.arguments,
25117
25181
  preview: toolArgumentsPreview(data.arguments, data.name),
@@ -25193,6 +25257,8 @@ function projectEvent(view, event) {
25193
25257
  view.anchors.stepStart.set(key, event.time);
25194
25258
  return {
25195
25259
  ...view,
25260
+ streaming: "",
25261
+ streamingReasoning: "",
25196
25262
  stats: {
25197
25263
  ...view.stats,
25198
25264
  steps: view.stats.steps + 1
@@ -25235,12 +25301,16 @@ function projectEvent(view, event) {
25235
25301
  if (appended.length === 0) return {
25236
25302
  ...view,
25237
25303
  busy: false,
25238
- busySince: 0
25304
+ busySince: 0,
25305
+ streaming: "",
25306
+ streamingReasoning: ""
25239
25307
  };
25240
25308
  return {
25241
25309
  ...view,
25242
25310
  busy: false,
25243
25311
  busySince: 0,
25312
+ streaming: "",
25313
+ streamingReasoning: "",
25244
25314
  entries: [...view.entries, ...appended]
25245
25315
  };
25246
25316
  }
@@ -25248,6 +25318,8 @@ function projectEvent(view, event) {
25248
25318
  const data = event.data;
25249
25319
  return {
25250
25320
  ...view,
25321
+ streaming: "",
25322
+ streamingReasoning: "",
25251
25323
  entries: [...view.entries, {
25252
25324
  kind: "retry",
25253
25325
  retryId: data.retryId,
@@ -25413,6 +25485,7 @@ function createReplayAccumulator() {
25413
25485
  streaming: "",
25414
25486
  streamingReasoning: "",
25415
25487
  todos: [],
25488
+ toolCallOrdinal: 0,
25416
25489
  busy: false,
25417
25490
  busySince: 0,
25418
25491
  model: "",
@@ -25527,11 +25600,13 @@ function replayProjectEvent(acc, event) {
25527
25600
  }
25528
25601
  }
25529
25602
  const text = textOf(message.content);
25603
+ const images = imagesOf(message.content);
25530
25604
  if (message.source.kind === "user") {
25531
25605
  appendReplayEntry(acc, {
25532
25606
  kind: "user",
25533
25607
  text,
25534
- notice: false
25608
+ notice: false,
25609
+ ...images.length === 0 ? {} : { images }
25535
25610
  });
25536
25611
  acc.stats = {
25537
25612
  ...acc.stats,
@@ -25574,11 +25649,13 @@ function replayProjectEvent(acc, event) {
25574
25649
  }
25575
25650
  }
25576
25651
  for (const message of inserted) {
25652
+ const images = imagesOf(message.content);
25577
25653
  appendReplayEntry(acc, {
25578
25654
  kind: "pending",
25579
25655
  messageId: message.id,
25580
25656
  target,
25581
- text: pendingText(message.content)
25657
+ text: pendingText(message.content),
25658
+ ...images.length === 0 ? {} : { images }
25582
25659
  });
25583
25660
  indexList(acc.pendingIndex, message.id).push(acc.entries.length - 1);
25584
25661
  ids.push(message.id);
@@ -25624,7 +25701,8 @@ function replayProjectEvent(acc, event) {
25624
25701
  appendReplayEntry(acc, {
25625
25702
  kind: "assistant",
25626
25703
  text,
25627
- reasoning
25704
+ reasoning,
25705
+ interrupted: event.data.interrupted === true ? true : void 0
25628
25706
  });
25629
25707
  acc.stats = {
25630
25708
  ...acc.stats,
@@ -25651,9 +25729,11 @@ function replayProjectEvent(acc, event) {
25651
25729
  const turnTools = acc.turnTools.get(data.turn) ?? /* @__PURE__ */ new Set();
25652
25730
  turnTools.add(data.callId);
25653
25731
  acc.turnTools.set(data.turn, turnTools);
25732
+ acc.toolCallOrdinal += 1;
25654
25733
  appendReplayEntry(acc, {
25655
25734
  kind: "tool",
25656
25735
  callId: data.callId,
25736
+ ordinal: acc.toolCallOrdinal,
25657
25737
  name: data.name,
25658
25738
  arguments: data.arguments,
25659
25739
  preview: toolArgumentsPreview(data.arguments, data.name),
@@ -25729,6 +25809,8 @@ function replayProjectEvent(acc, event) {
25729
25809
  }
25730
25810
  acc.turnSteps.set(event.data.turn, key);
25731
25811
  acc.stepStart.set(key, event.time);
25812
+ acc.streaming = "";
25813
+ acc.streamingReasoning = "";
25732
25814
  acc.stats = {
25733
25815
  ...acc.stats,
25734
25816
  steps: acc.stats.steps + 1
@@ -25738,6 +25820,8 @@ function replayProjectEvent(acc, event) {
25738
25820
  case "turn/end": {
25739
25821
  const reason = event.data.reason;
25740
25822
  const appended = [];
25823
+ acc.streamingReasoning = "";
25824
+ acc.streaming = "";
25741
25825
  if (reason.kind === "error") {
25742
25826
  const recovery = reason.error.code === "MISSING_CREDENTIAL" ? " · open /model to add an API key" : "";
25743
25827
  appended.push({
@@ -25775,6 +25859,8 @@ function replayProjectEvent(acc, event) {
25775
25859
  }
25776
25860
  case "llm/retry": {
25777
25861
  const data = event.data;
25862
+ acc.streaming = "";
25863
+ acc.streamingReasoning = "";
25778
25864
  appendReplayEntry(acc, {
25779
25865
  kind: "retry",
25780
25866
  retryId: data.retryId,
@@ -25915,6 +26001,7 @@ function finishReplay(acc) {
25915
26001
  streaming: acc.streaming,
25916
26002
  streamingReasoning: acc.streamingReasoning,
25917
26003
  todos: acc.todos,
26004
+ toolCallOrdinal: acc.toolCallOrdinal,
25918
26005
  busy: acc.busy,
25919
26006
  busySince: acc.busySince,
25920
26007
  model: acc.model,
@@ -26001,34 +26088,94 @@ function visibleColumns(text) {
26001
26088
  }
26002
26089
  return columns;
26003
26090
  }
26004
- /** Walk a segment list, breaking it into lines that fit `width` columns. */
26091
+ /** Punctuation that should not become the first visible glyph of a row. */
26092
+ function isClosingPunctuation(text) {
26093
+ return /^[,.;:!?%、。,.!?:;%)》」』】〕〉》”’]/u.test(text);
26094
+ }
26095
+ /**
26096
+ * Split prose into soft-wrap units: ASCII words stay intact, while wide CJK
26097
+ * glyphs become individual break opportunities. This keeps ordinary prose
26098
+ * readable without allowing a Chinese paragraph to become one overlong word.
26099
+ */
26100
+ function wrapUnits(segments) {
26101
+ const units = [];
26102
+ for (const segment of segments) {
26103
+ let word = "";
26104
+ const flushWord = () => {
26105
+ if (word !== "") units.push({
26106
+ text: word,
26107
+ style: segment.style
26108
+ });
26109
+ word = "";
26110
+ };
26111
+ for (const char of segment.text) if (char === " ") {
26112
+ flushWord();
26113
+ units.push({
26114
+ text: char,
26115
+ style: segment.style
26116
+ });
26117
+ } else if (visibleColumns(char) > 1) {
26118
+ flushWord();
26119
+ units.push({
26120
+ text: char,
26121
+ style: segment.style
26122
+ });
26123
+ } else word += char;
26124
+ flushWord();
26125
+ }
26126
+ return units;
26127
+ }
26128
+ /** Walk styled units, keeping logical rows within the physical column budget. */
26005
26129
  function wrapSegments(segments, width) {
26130
+ const limit = Math.max(1, Math.floor(width));
26006
26131
  const lines = [];
26007
26132
  let current = [];
26008
26133
  let used = 0;
26009
- for (const segment of segments) {
26010
- const words = segment.text.split(/( )/u);
26011
- for (const word of words) {
26012
- if (word === "") continue;
26013
- const columns = visibleColumns(word);
26014
- if (used + columns > width && used > 0) {
26015
- lines.push(current);
26016
- current = [];
26017
- used = 0;
26018
- }
26019
- current.push({
26020
- text: word,
26021
- style: segment.style
26134
+ const flush = () => {
26135
+ while (current.at(-1)?.text === " ") used -= visibleColumns(current.pop().text);
26136
+ if (current.length > 0) lines.push(merge(current));
26137
+ current = [];
26138
+ used = 0;
26139
+ };
26140
+ const appendAtom = (unit) => {
26141
+ const columns = visibleColumns(unit.text);
26142
+ if (used > 0 && used + columns > limit) flush();
26143
+ current.push(unit);
26144
+ used += columns;
26145
+ };
26146
+ const append = (unit) => {
26147
+ const columns = visibleColumns(unit.text);
26148
+ if (used === 0 && columns > limit) {
26149
+ for (const char of unit.text) appendAtom({
26150
+ text: char,
26151
+ style: unit.style
26022
26152
  });
26153
+ return;
26154
+ }
26155
+ if (used + columns <= limit || current.length === 0) {
26156
+ current.push(unit);
26023
26157
  used += columns;
26158
+ return;
26024
26159
  }
26025
- }
26026
- if (current.length > 0) lines.push(current);
26027
- return lines.map((line) => {
26028
- const last = line[line.length - 1];
26029
- if (last !== void 0 && last.text === " " && line.length > 1) return line.slice(0, -1);
26030
- return line;
26031
- });
26160
+ const previous = current.at(-1);
26161
+ if (isClosingPunctuation(unit.text) && previous !== void 0 && visibleColumns(previous.text) > 1) {
26162
+ current.pop();
26163
+ used -= visibleColumns(previous.text);
26164
+ flush();
26165
+ appendAtom(previous);
26166
+ appendAtom(unit);
26167
+ return;
26168
+ }
26169
+ flush();
26170
+ if (columns > limit) for (const char of unit.text) appendAtom({
26171
+ text: char,
26172
+ style: unit.style
26173
+ });
26174
+ else appendAtom(unit);
26175
+ };
26176
+ for (const unit of wrapUnits(segments)) append(unit);
26177
+ flush();
26178
+ return lines;
26032
26179
  }
26033
26180
  /** Join adjacent same-style runs so the app renders fewer elements. */
26034
26181
  function merge(segments) {
@@ -26395,10 +26542,14 @@ function renderTableRecords(table, width) {
26395
26542
  return lines;
26396
26543
  }
26397
26544
  /** Render markdown text into styled lines of at most `width` columns. */
26398
- function renderMarkdown(text, width) {
26545
+ function renderMarkdown(text, width, options = {}) {
26399
26546
  const lines = [];
26400
26547
  let separatorPending = false;
26401
26548
  const push = (segments) => {
26549
+ if (options.physicalWrap === false) {
26550
+ lines.push({ segments: merge(segments) });
26551
+ return;
26552
+ }
26402
26553
  for (const wrapped of wrapSegments(segments, Math.max(10, width))) lines.push({ segments: merge(wrapped) });
26403
26554
  };
26404
26555
  const startBlock = () => {
@@ -26450,12 +26601,12 @@ function renderMarkdown(text, width) {
26450
26601
  }
26451
26602
  const ordered = ORDERED.exec(line);
26452
26603
  if (ordered !== null) {
26453
- push([seg(` ${ordered[1] ?? ""}. `, "accent"), ...parseInline(ordered[2] ?? "").map((run) => seg(run.text, run.style))]);
26604
+ push([seg(options.physicalWrap === false ? `${ordered[1] ?? ""}. ` : ` ${ordered[1] ?? ""}. `, "accent"), ...parseInline(ordered[2] ?? "").map((run) => seg(run.text, run.style))]);
26454
26605
  continue;
26455
26606
  }
26456
26607
  const unordered = UNORDERED.exec(line);
26457
26608
  if (unordered !== null) {
26458
- push([seg(" • ", "accent"), ...parseInline(unordered[1] ?? "").map((run) => seg(run.text, run.style))]);
26609
+ push([seg(options.physicalWrap === false ? "• " : " • ", "accent"), ...parseInline(unordered[1] ?? "").map((run) => seg(run.text, run.style))]);
26459
26610
  continue;
26460
26611
  }
26461
26612
  const paragraph = [line];
@@ -26477,21 +26628,6 @@ function renderMarkdown(text, width) {
26477
26628
  }
26478
26629
  //#endregion
26479
26630
  //#region src/render/animations.ts
26480
- /** Single-cell stepped pulse: flat holds mirroring the web's 125ms keyframes. */
26481
- const PULSE_FRAMES = [
26482
- "█",
26483
- "█",
26484
- "▆",
26485
- "▃",
26486
- "▁",
26487
- "▃",
26488
- "▆",
26489
- "█"
26490
- ];
26491
- /** Pulse frame for a monotonic tick. */
26492
- function pulseFrame(tick) {
26493
- return PULSE_FRAMES[tick % PULSE_FRAMES.length] ?? PULSE_FRAMES[0];
26494
- }
26495
26631
  /**
26496
26632
  * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
26497
26633
  * ring trail clockwise around the eight outer positions, one braille glyph
@@ -26515,7 +26651,7 @@ function busyChaseFrame(tick) {
26515
26651
  function caretVisible(tick) {
26516
26652
  return tick % 2 === 0;
26517
26653
  }
26518
- /** All styles in canonical order, for random selection and tests. */
26654
+ /** All styles in canonical order, for random selection. */
26519
26655
  const DEEPSEEK_WAVE_STYLES = [
26520
26656
  "wave",
26521
26657
  "aurora",
@@ -26523,6 +26659,9 @@ const DEEPSEEK_WAVE_STYLES = [
26523
26659
  ];
26524
26660
  /** Pulse ring half-width in columns — Codex PULSE_HALF_WIDTH (4.5). */
26525
26661
  const PULSE_HALF_WIDTH = 4.5;
26662
+ /** Sparkle start and frame cadence — Codex SPARK_START / SPARK_FRAME. */
26663
+ const SPARK_START_MS = 900;
26664
+ const SPARK_FRAME_MS = 100;
26526
26665
  /** Sparkle glyphs in frame order — Codex SPARK_GLYPHS (`· ✦ ✧`). */
26527
26666
  const SPARK_GLYPHS = [
26528
26667
  "·",
@@ -26544,6 +26683,15 @@ const DEEPSEEK_WAVE_BANDS = {
26544
26683
  .35,
26545
26684
  .55,
26546
26685
  1
26686
+ ]],
26687
+ unknown: [[
26688
+ .1,
26689
+ .7,
26690
+ 1
26691
+ ], [
26692
+ .35,
26693
+ .55,
26694
+ 1
26547
26695
  ]]
26548
26696
  },
26549
26697
  aurora: {
@@ -26572,6 +26720,23 @@ const DEEPSEEK_WAVE_BANDS = {
26572
26720
  .35,
26573
26721
  2
26574
26722
  ]
26723
+ ],
26724
+ unknown: [
26725
+ [
26726
+ .35,
26727
+ .15,
26728
+ 0
26729
+ ],
26730
+ [
26731
+ -.5,
26732
+ .6,
26733
+ 1
26734
+ ],
26735
+ [
26736
+ .75,
26737
+ .35,
26738
+ 2
26739
+ ]
26575
26740
  ]
26576
26741
  },
26577
26742
  pulse: {
@@ -26588,15 +26753,27 @@ const DEEPSEEK_WAVE_BANDS = {
26588
26753
  .45,
26589
26754
  .55,
26590
26755
  1.1
26756
+ ]],
26757
+ unknown: [[
26758
+ .1,
26759
+ .55,
26760
+ .8
26761
+ ], [
26762
+ .45,
26763
+ .55,
26764
+ 1.1
26591
26765
  ]]
26592
26766
  }
26593
26767
  };
26768
+ /** Extra display time applied to every Codex ignition style. */
26769
+ const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200;
26594
26770
  /** Original Codex duration used as the animation's sampling timeline. */
26595
26771
  function deepseekWaveBaseDuration(tier, style) {
26772
+ const pro = tier === "deepseek" || tier === "unknown";
26596
26773
  switch (style) {
26597
- case "aurora": return tier === "flash" ? 1300 : 1600;
26598
- case "pulse": return tier === "flash" ? 900 : 1250;
26599
- case "wave": return tier === "flash" ? 1e3 : 1300;
26774
+ case "aurora": return pro ? 1600 : 1300;
26775
+ case "pulse": return pro ? 1250 : 900;
26776
+ case "wave": return pro ? 1300 : 1e3;
26600
26777
  }
26601
26778
  }
26602
26779
  /**
@@ -26607,7 +26784,7 @@ function deepseekWaveBaseDuration(tier, style) {
26607
26784
  * @returns the duration in milliseconds.
26608
26785
  */
26609
26786
  function deepseekWaveDuration(tier, style = "wave") {
26610
- return deepseekWaveBaseDuration(tier, style) + 200;
26787
+ return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS;
26611
26788
  }
26612
26789
  /** Map the extended display timeline back onto the original Codex samples. */
26613
26790
  function deepseekWaveSampleElapsedMs(tick, tier, style) {
@@ -26625,22 +26802,6 @@ function deepseekWaveStyleRandom(previous) {
26625
26802
  return candidates[Math.floor(Math.random() * candidates.length)] ?? "wave";
26626
26803
  }
26627
26804
  /**
26628
- * Blank-cell background the wave tint blends toward — fixed approximations
26629
- * of the terminal's default background, mirroring Codex's
26630
- * `user_message_bg_rgb` (which derives a near-black / near-white bubble tint
26631
- * from the terminal background). The Ink layer picks the active theme's one.
26632
- */
26633
- const WAVE_BASE_DARK = [
26634
- 16,
26635
- 18,
26636
- 24
26637
- ];
26638
- const WAVE_BASE_LIGHT = [
26639
- 242,
26640
- 244,
26641
- 248
26642
- ];
26643
- /**
26644
26805
  * Tier for a `provider/model` label: a model id containing `flash` runs the
26645
26806
  * single-band flash tier; everything else (pro/reasoner/chat) runs the
26646
26807
  * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
@@ -26734,14 +26895,22 @@ function blendRgb(fg, bg, alpha) {
26734
26895
  ];
26735
26896
  }
26736
26897
  /**
26737
- * The background color for one composer-row column at a tick Codex
26898
+ * Per-row phase share of the duration: the crest reaches the top row first
26899
+ * and the bottom row last, sweeping down the band. 0.12 keeps the bottom
26900
+ * row's lag inside the 200ms duration extension.
26901
+ */
26902
+ const DEEPSEEK_WAVE_ROW_PHASE = .12;
26903
+ /**
26904
+ * The background color for one composer-band column at a tick — Codex
26738
26905
  * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
26739
26906
  * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
26740
26907
  * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
26741
26908
  * blends the mixed hue toward the blank-cell base at the style's alpha cap,
26742
26909
  * and Aurora applies its own fade envelope. Returns `null` when the column
26743
26910
  * should stay transparent, so the row returns to no `backgroundColor` on
26744
- * both ends.
26911
+ * both ends. With `rows > 1` each row samples the same timeline shifted by a
26912
+ * per-row phase offset, so the crest cascades down the band instead of
26913
+ * painting every row identically.
26745
26914
  * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
26746
26915
  * @param column - column index in the content row (0..width-1).
26747
26916
  * @param width - content-row width in columns.
@@ -26749,11 +26918,13 @@ function blendRgb(fg, bg, alpha) {
26749
26918
  * @param style - the ignition style.
26750
26919
  * @param hues - the tier's three hues.
26751
26920
  * @param base - the blank-cell base color the tint blends toward.
26921
+ * @param row - row index in the band (0..rows-1; default 0 = old single-row).
26922
+ * @param rows - band height in rows (default 1).
26752
26923
  * @returns the blended RGB background, or null for transparent.
26753
26924
  */
26754
- function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
26925
+ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row = 0, rows = 1) {
26755
26926
  const total = deepseekWaveBaseDuration(tier, style) / 1e3;
26756
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3;
26927
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3 - (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE;
26757
26928
  const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
26758
26929
  const weights = [
26759
26930
  0,
@@ -26792,27 +26963,11 @@ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
26792
26963
  */
26793
26964
  function deepseekWaveSpark(tick) {
26794
26965
  const elapsed = deepseekWaveSampleElapsedMs(tick, "deepseek", "wave");
26795
- if (elapsed < 900) return null;
26796
- const frame = Math.floor((elapsed - 900) / 100);
26966
+ if (elapsed < SPARK_START_MS) return null;
26967
+ const frame = Math.floor((elapsed - SPARK_START_MS) / SPARK_FRAME_MS);
26797
26968
  return SPARK_GLYPHS[frame] ?? null;
26798
26969
  }
26799
26970
  /**
26800
- * The composer BORDER color at a tick: the frame breathes with the wave —
26801
- * the palette's static dim blends toward the tier accent as the crest is
26802
- * alive and back, so the frame glows up while the wave sweeps and settles to
26803
- * dim on both ends (first==last frame==dim, no hard jump).
26804
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
26805
- * @param tier - the wave tier.
26806
- * @param hues - the tier's three hues; the border blends toward hues[0].
26807
- * @param dim - the palette's static dim RGB (the resting border color).
26808
- * @returns the blended border RGB.
26809
- */
26810
- function deepseekWaveBorderColor(tick, tier, style, hues, dim) {
26811
- const total = deepseekWaveBaseDuration(tier, style) / 1e3;
26812
- const glow = envelope(deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3, total, total * .25, total * .4);
26813
- return blendRgb(hues[0], dim, .25 + glow * .6);
26814
- }
26815
- /**
26816
26971
  * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
26817
26972
  * shortly after the first crest launches and out before the wave settles,
26818
26973
  * so the brand name surfaces through the sweep's middle. The Ink layer
@@ -26848,14 +27003,46 @@ function isOfficialDeepSeekLabel(label) {
26848
27003
  const model = slash < 0 ? "" : label.slice(slash + 1);
26849
27004
  return provider.toLowerCase().includes("deepseek") || model.toLowerCase().includes("deepseek");
26850
27005
  }
27006
+ /**
27007
+ * Known reasoning-effort ranks in ascending order. Effort ids are opaque
27008
+ * adapter-owned strings, so the rank table covers the conventional names
27009
+ * (off → low → medium → high → xhigh → max/ultra); an unrecognized id
27010
+ * ranks as unknown (0), which never triggers the high-effort wave.
27011
+ */
27012
+ const EFFORT_RANK = {
27013
+ off: 0,
27014
+ none: 0,
27015
+ low: 1,
27016
+ medium: 2,
27017
+ med: 2,
27018
+ high: 3,
27019
+ xhigh: 4,
27020
+ "x-high": 4,
27021
+ "very-high": 4,
27022
+ max: 5,
27023
+ maximum: 5,
27024
+ ultra: 5
27025
+ };
27026
+ /**
27027
+ * True when an effective reasoning effort is STRICTLY above `high` — the
27028
+ * trigger gate for the "Into the Unknown" wave on non-DeepSeek routes.
27029
+ * Absent efforts and unrecognized ids never qualify.
27030
+ * @param effort - the effective reasoning-effort id ('' or undefined when none).
27031
+ * @returns whether the effort ranks above high.
27032
+ */
27033
+ function effortAboveHigh(effort) {
27034
+ if (effort === void 0 || effort === "") return false;
27035
+ const rank = EFFORT_RANK[effort.trim().toLowerCase()];
27036
+ return rank !== void 0 && rank > 3;
27037
+ }
26851
27038
  //#endregion
26852
27039
  //#region src/session-directory.ts
26853
27040
  /** Lightweight session-directory projection for the /resume picker. */
26854
27041
  /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
26855
27042
  const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
26856
- /** True when the header describes a subagent conversation (durable lineage). */
27043
+ /** True only for delegated subagents; ordinary forks also carry lineage. */
26857
27044
  function isSubagentSession(header) {
26858
- return header.origin === "subagent" || header.parentSession !== void 0;
27045
+ return header.origin === "subagent";
26859
27046
  }
26860
27047
  function comparablePath(value) {
26861
27048
  const resolved = resolve(value);
@@ -27075,10 +27262,18 @@ function cacheHitPercent(usage) {
27075
27262
  const STATUS_ITEM_SEPARATOR = " · ";
27076
27263
  /** The Codex-style mode cycle hint appended to the permission badge. */
27077
27264
  const STATUS_CYCLE_HINT = " (shift+tab to cycle)";
27265
+ /** Occupancy at which the usage readout flips from brand blue to amber. */
27266
+ const CONTEXT_WARN_PERCENT = 90;
27078
27267
  /** Free-tail floor in columns: wide enough for the bare percent readout, so
27079
27268
  * the warning stays visible even at 100%+ occupancy. */
27080
27269
  const CONTEXT_MIN_FREE = 5;
27081
27270
  /**
27271
+ * Narrowest bar width the drop ladder tries before giving up on the context
27272
+ * group: the bar shrinks inside its own budget first (a few columns still
27273
+ * show the bare percent readout) and only then drops as a whole.
27274
+ */
27275
+ const CONTEXT_MIN_WIDTH = 5;
27276
+ /**
27082
27277
  * Render context occupancy as ONE stepless bar: a solid DeepSeek-blue fill
27083
27278
  * run, a dim dotted free track, and the usage readout riding the track's
27084
27279
  * right edge (`12.3K/1.0M 25%`, shrinking to the bare percent as the track
@@ -27096,7 +27291,7 @@ function contextBar(usedTokens, contextWindow, width) {
27096
27291
  if (width <= 0 || contextWindow <= 0) return [];
27097
27292
  const used = Math.max(0, usedTokens);
27098
27293
  const percent = Math.round(used / contextWindow * 100);
27099
- const readoutTone = percent >= 90 ? "warn" : "value";
27294
+ const readoutTone = percent >= CONTEXT_WARN_PERCENT ? "warn" : "value";
27100
27295
  const freeShare = Math.round(Math.max(0, contextWindow - used) / contextWindow * width);
27101
27296
  const freeColumns = Math.min(width, Math.max(freeShare, CONTEXT_MIN_FREE));
27102
27297
  const usedColumns = Math.max(0, width - freeColumns);
@@ -27288,7 +27483,7 @@ function joinWidth(parts, separator) {
27288
27483
  return width + separator * (parts.length - 1);
27289
27484
  }
27290
27485
  /** Build every candidate group/span with its drop rank and item id. */
27291
- function buildCandidates(facts, stats, busy, enabled) {
27486
+ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
27292
27487
  const identity = [{
27293
27488
  text: busy ? "● " : "○ ",
27294
27489
  tone: busy ? "live" : "meta"
@@ -27410,7 +27605,7 @@ function buildCandidates(facts, stats, busy, enabled) {
27410
27605
  group: { spans: [{
27411
27606
  text: "context ",
27412
27607
  tone: "label"
27413
- }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, 24)] },
27608
+ }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, contextWidth)] },
27414
27609
  rank: RANK_CONTEXT,
27415
27610
  id: "context"
27416
27611
  });
@@ -27499,7 +27694,8 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27499
27694
  const items = options.items ?? DEFAULT_STATUSLINE_ITEMS;
27500
27695
  const enabled = new Set(items);
27501
27696
  const budget = Math.max(1, Math.floor(columns) - WIDTH_SAFETY);
27502
- const { left, right, badge, row2 } = buildCandidates(facts, stats, busy, enabled);
27697
+ const maxContextWidth = Math.max(CONTEXT_MIN_WIDTH, Math.min(budget, Math.floor(options.contextWidth ?? 24)));
27698
+ const { left, right, badge, row2 } = buildCandidates(facts, stats, busy, enabled, maxContextWidth);
27503
27699
  const groupSeparator = visibleColumns(" | ");
27504
27700
  const itemSeparator = visibleColumns(STATUS_ITEM_SEPARATOR);
27505
27701
  const position = new Map(items.map((id, index) => [id, index]));
@@ -27510,12 +27706,51 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27510
27706
  let hint = badge >= 0 && !busy;
27511
27707
  const leftKept = [...orderedLeft];
27512
27708
  const rightKept = [...orderedRight];
27709
+ let contextWidth = maxContextWidth;
27710
+ const rebuildContext = () => {
27711
+ const index = leftKept.findIndex((entry) => entry.id === "context");
27712
+ if (index < 0) return;
27713
+ leftKept[index] = {
27714
+ group: { spans: [{
27715
+ text: "context ",
27716
+ tone: "label"
27717
+ }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, contextWidth)] },
27718
+ rank: RANK_CONTEXT,
27719
+ id: "context"
27720
+ };
27721
+ };
27513
27722
  const width = () => {
27514
27723
  const leftWidth = joinWidth(leftKept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
27515
27724
  const rightWidth = joinWidth(rightKept.map((entry) => visibleColumns(entry.span.text)), itemSeparator) + (hint ? visibleColumns(STATUS_CYCLE_HINT) : 0);
27516
27725
  return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth;
27517
27726
  };
27518
27727
  while (width() > budget) {
27728
+ if (leftKept.some((entry) => entry.id === "context") && contextWidth > CONTEXT_MIN_WIDTH) {
27729
+ const overflow = width() - budget;
27730
+ contextWidth = Math.max(CONTEXT_MIN_WIDTH, contextWidth - overflow);
27731
+ rebuildContext();
27732
+ continue;
27733
+ }
27734
+ const contextIndex = leftKept.findIndex((entry) => entry.id === "context");
27735
+ if (contextIndex >= 0) {
27736
+ leftKept.splice(contextIndex, 1);
27737
+ continue;
27738
+ }
27739
+ if (hint && rightKept.length > 0 && leftKept.length > 0) {
27740
+ const identity = leftKept[0];
27741
+ const identityText = identity.group.spans.map((span) => span.text).join("");
27742
+ const identityBudget = budget - joinWidth(rightKept.map((entry) => visibleColumns(entry.span.text)), itemSeparator) - LEFT_RIGHT_GAP - visibleColumns(STATUS_CYCLE_HINT);
27743
+ if (identityBudget > 0 && visibleColumns(identityText) > identityBudget) {
27744
+ leftKept[0] = {
27745
+ ...identity,
27746
+ group: { spans: [{
27747
+ text: truncateColumns(identityText, identityBudget),
27748
+ tone: "model"
27749
+ }] }
27750
+ };
27751
+ continue;
27752
+ }
27753
+ }
27519
27754
  if (hint) {
27520
27755
  hint = false;
27521
27756
  continue;
@@ -27586,6 +27821,7 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27586
27821
  }
27587
27822
  //#endregion
27588
27823
  //#region src/render/lines.ts
27824
+ /** Width-safe styled physical rows for bounded terminal panels. */
27589
27825
  /** Construct one segment without leaking mutable objects into cached rows. */
27590
27826
  function lineSegment(text, style = "plain") {
27591
27827
  return {
@@ -27643,6 +27879,59 @@ function styledLines(segments, columns) {
27643
27879
  function textLines(text, columns, style = "plain") {
27644
27880
  return styledLines([lineSegment(text, style)], columns);
27645
27881
  }
27882
+ /** Prefix every wrapped physical row without exceeding the column budget. */
27883
+ function prefixedStyledLines(segments, columns, prefix, prefixStyle = "plain") {
27884
+ const width = Math.max(1, Math.floor(columns));
27885
+ const prefixWidth = Math.min(width, visibleColumns(prefix));
27886
+ return styledLines(segments, Math.max(1, width - prefixWidth)).map((line) => ({ segments: [lineSegment(prefix, prefixStyle), ...line.segments] }));
27887
+ }
27888
+ /** Text convenience for a tool row whose continuation must keep its gutter. */
27889
+ function prefixedTextLines(text, columns, prefix, style = "plain") {
27890
+ return prefixedStyledLines([lineSegment(text, style)], columns, prefix, style);
27891
+ }
27892
+ /**
27893
+ * Wrap styled segments with a hanging indent: the first physical row carries
27894
+ * `firstPrefix` (often a marker plus gutter) and every wrapped continuation
27895
+ * carries the narrower `contPrefix`, so long tool summaries and prompts
27896
+ * align under their card instead of falling back to column zero. The first
27897
+ * row may hold one prefix-width more than the continuations.
27898
+ */
27899
+ function hangingStyledLines(segments, columns, firstPrefix, firstStyle, contPrefix, contStyle = firstStyle) {
27900
+ const width = Math.max(2, Math.floor(columns));
27901
+ const firstPrefixText = truncateColumns(firstPrefix, Math.max(1, width - 1));
27902
+ const contPrefixText = truncateColumns(contPrefix, Math.max(1, width - 1));
27903
+ const firstBudget = Math.max(1, width - visibleColumns(firstPrefixText));
27904
+ const contBudget = Math.max(1, width - visibleColumns(contPrefixText));
27905
+ const lines = [];
27906
+ let current = [];
27907
+ let used = 0;
27908
+ let budget = firstBudget;
27909
+ const flush = () => {
27910
+ lines.push({ segments: current });
27911
+ current = [];
27912
+ used = 0;
27913
+ budget = contBudget;
27914
+ };
27915
+ for (const segment of segments) {
27916
+ const safe = displayText(segment.text).replaceAll(" ", " ").replaceAll("\r", "");
27917
+ for (const char of safe) {
27918
+ if (char === "\n") {
27919
+ flush();
27920
+ continue;
27921
+ }
27922
+ const cells = visibleColumns(char);
27923
+ if (used > 0 && used + cells > budget) flush();
27924
+ appendSegment(current, char, segment.style);
27925
+ used += cells;
27926
+ }
27927
+ }
27928
+ if (current.length > 0 || lines.length === 0) flush();
27929
+ return lines.map((line, index) => ({ segments: [lineSegment(index === 0 ? firstPrefixText : contPrefixText, index === 0 ? firstStyle : contStyle), ...line.segments] }));
27930
+ }
27931
+ /** Plain-text convenience over {@link hangingStyledLines}. */
27932
+ function hangingTextLines(text, columns, firstPrefix, firstStyle = "plain", contPrefix = " ", contStyle = firstStyle) {
27933
+ return hangingStyledLines([lineSegment(text, firstStyle)], columns, firstPrefix, firstStyle, contPrefix, contStyle);
27934
+ }
27646
27935
  /** Markdown rows re-hardened so a single long word cannot escape the budget. */
27647
27936
  function markdownLines(text, columns) {
27648
27937
  const width = Math.max(1, Math.floor(columns));
@@ -27660,68 +27949,517 @@ function reasoningLines(text, columns) {
27660
27949
  return displayText(text).replaceAll(" ", " ").replaceAll("\r", "").split("\n").flatMap((line) => styledLines([lineSegment(line, "dimItalic")], contentWidth)).map((line, index) => ({ segments: [lineSegment(index === 0 ? "✻ " : " ", "dimItalic"), ...line.segments] }));
27661
27950
  }
27662
27951
  /** Expanded structured tool detail as scrollable, width-safe rows. */
27952
+ /**
27953
+ * Detail rows share the tool card's four-column hanging gutter: the summary
27954
+ * (⎿) and delegation prompt (└) continuations already sit at four columns, so
27955
+ * diff/read/web/raw rows align under them instead of floating two columns
27956
+ * shallower.
27957
+ */
27663
27958
  function toolDetailLines(detail, columns) {
27664
27959
  switch (detail.kind) {
27665
- case "diff": return detail.diffs.flatMap((diff) => [...styledLines([
27666
- lineSegment(" ── ", "dim"),
27667
- lineSegment(diff.path, "dim"),
27668
- lineSegment(diff.truncated ? " (diff truncated)" : "", "dim")
27669
- ], columns), ...diff.lines.flatMap((line) => styledLines([lineSegment(` ${line.mark}${line.text}`, line.mark === "+" ? "success" : line.mark === "-" ? "error" : "dim")], columns))]);
27670
- case "read": return [...textLines(` ── ${detail.path} · lines ${detail.offset}-${detail.lines.length > 0 ? detail.lines[detail.lines.length - 1].number : detail.offset - 1} of ${detail.totalLines}${detail.truncated ? " (window truncated)" : ""}`, columns, "dim"), ...detail.lines.flatMap((line) => textLines(` ${String(line.number).padStart(5, " ")} | ${line.text}`, columns, "dim"))];
27671
- case "web-search": return [...detail.sources.flatMap((source) => [...styledLines([lineSegment(` ? ${source.title ?? source.url}`, "brand"), lineSegment(` - ${source.url}`, "dim")], columns), ...source.snippet === "" ? [] : textLines(` ${source.snippet}`, columns, "dim")]), ...textLines(` ${detail.sources.length} sources${detail.truncated ? " (capped)" : ""}`, columns, "dim")];
27672
- case "web-fetch": return textLines(` ${detail.url} · HTTP ${detail.statusCode}`, columns, "dim");
27673
- case "raw": return [...textLines(` ${detail.text}`, columns, "dim"), ...textLines(detail.truncated ? " … (output truncated)" : " (end of output)", columns, "dim")];
27960
+ case "diff": return detail.diffs.flatMap((diff) => [...prefixedTextLines(`${diff.path}${diff.truncated ? " (diff truncated)" : ""}`, columns, " ── ", "dim"), ...diff.lines.flatMap((line) => prefixedTextLines(`${line.mark}${line.text}`, columns, " ", line.mark === "+" ? "success" : line.mark === "-" ? "error" : "dim"))]);
27961
+ case "read": return [...prefixedTextLines(`${detail.path} · lines ${detail.offset}-${detail.lines.length > 0 ? detail.lines[detail.lines.length - 1].number : detail.offset - 1} of ${detail.totalLines}${detail.truncated ? " (window truncated)" : ""}`, columns, " ── ", "dim"), ...detail.lines.flatMap((line) => prefixedTextLines(`${String(line.number).padStart(5, " ")} | ${line.text}`, columns, " ", "dim"))];
27962
+ case "web-search": return [...detail.sources.flatMap((source) => [...prefixedStyledLines([lineSegment(source.title ?? source.url, "brand"), lineSegment(` - ${source.url}`, "dim")], columns, " ? "), ...source.snippet === "" ? [] : prefixedTextLines(source.snippet, columns, " ", "dim")]), ...prefixedTextLines(`${detail.sources.length} sources${detail.truncated ? " (capped)" : ""}`, columns, " ", "dim")];
27963
+ case "web-fetch": return prefixedTextLines(`${detail.url} · HTTP ${detail.statusCode}`, columns, " ", "dim");
27964
+ case "raw": return [...prefixedTextLines(detail.text, columns, " ", "dim"), ...prefixedTextLines(detail.truncated ? "… (output truncated)" : "(end of output)", columns, " ", "dim")];
27674
27965
  default: return detail;
27675
27966
  }
27676
27967
  }
27677
27968
  /**
27678
27969
  * Convert one durable transcript entry to its complete scrollable row model.
27679
27970
  * The source entry stays intact; only the caller's visible slice is rendered.
27971
+ * Wrapped continuations keep a hanging indent aligned under each row's
27972
+ * content (Codex history-cell alignment) instead of resetting to column 0.
27680
27973
  */
27681
- function transcriptEntryLines(entry, columns) {
27974
+ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true) {
27682
27975
  const width = Math.max(1, Math.floor(columns));
27683
27976
  switch (entry.kind) {
27684
- case "user": return styledLines([lineSegment(entry.notice ? "⤷ " : " ", entry.notice ? "dim" : "brand"), lineSegment(entry.text, entry.notice ? "dim" : "plain")], width);
27685
- case "pending": return styledLines([lineSegment("❯ ", "brand"), lineSegment(entry.text, "plain")], width);
27977
+ case "user": return entry.notice ? hangingStyledLines([lineSegment(promptDisplayText(entry), "dim")], width, "⤷ ", "dim", " ", "dim") : hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
27978
+ case "pending": return hangingStyledLines([lineSegment(promptDisplayText(entry), "plain")], width, "❯ ", "brand", " ", "plain");
27686
27979
  case "assistant": {
27687
- const reasoning = entry.reasoning === "" ? [] : reasoningLines(entry.reasoning, width);
27980
+ const reasoning = entry.reasoning === "" ? [] : showReasoning ? reasoningLines(entry.reasoning, width) : textLines(`✻ Thinking (${entry.reasoning.length} chars${reasoningToggleHint ? ", Ctrl+R to expand" : ""})`, width, "dim");
27688
27981
  const body = markdownLines(entry.text, Math.max(10, width - 2)).map((line) => ({ segments: [{
27689
27982
  text: " ",
27690
27983
  style: "plain"
27691
27984
  }, ...line.segments] }));
27692
- return [...reasoning, ...body];
27985
+ const interrupted = entry.interrupted === true ? textLines(" ⏹ interrupted", width, "dim") : [];
27986
+ return [
27987
+ ...reasoning,
27988
+ ...body,
27989
+ ...interrupted
27990
+ ];
27693
27991
  }
27694
27992
  case "tool": {
27695
27993
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27696
27994
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
27995
+ const summaryStyle = entry.state === "error" ? "error" : "dim";
27697
27996
  return [
27698
- ...styledLines([
27699
- lineSegment(`${mark} `, markStyle),
27997
+ ...hangingStyledLines([
27998
+ lineSegment(`[${entry.ordinal}] `, "dim"),
27700
27999
  lineSegment(entry.name, "brand"),
27701
28000
  lineSegment(entry.preview === "" ? "" : ` ${entry.preview}`, "dim")
27702
- ], width),
27703
- ...entry.prompt === "" ? [] : textLines(` └ ${entry.prompt}`, width, "dim"),
27704
- ...entry.summary === "" ? [] : textLines(` ${entry.summary}`, width, entry.state === "error" ? "error" : "dim"),
28001
+ ], width, `${mark} `, markStyle, " ", "plain"),
28002
+ ...entry.prompt === "" ? [] : hangingTextLines(entry.prompt, width, " └ ", "dim", " "),
28003
+ ...entry.summary === "" ? [] : hangingTextLines(entry.state === "error" ? `call ${entry.ordinal}: ${entry.summary}` : entry.summary, width, "", summaryStyle, " ", summaryStyle),
27705
28004
  ...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
27706
28005
  ];
27707
28006
  }
27708
28007
  case "command": {
27709
28008
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27710
28009
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
27711
- return [...styledLines([
27712
- lineSegment(`${mark} `, markStyle),
27713
- lineSegment(`/${entry.name}`, "brand"),
27714
- lineSegment(entry.args === "" ? "" : ` ${entry.args}`, "dim")
27715
- ], width), ...entry.summary === "" ? [] : textLines(` ⎿ ${entry.summary}`, width, entry.state === "error" ? "error" : "dim")];
28010
+ const summaryStyle = entry.state === "error" ? "error" : "dim";
28011
+ return [...hangingStyledLines([lineSegment(`/${entry.name}`, "brand"), lineSegment(entry.args === "" ? "" : ` ${entry.args}`, "dim")], width, `${mark} `, markStyle, " ", "plain"), ...entry.summary === "" ? [] : hangingTextLines(entry.summary, width, " ⎿ ", summaryStyle, " ", summaryStyle)];
27716
28012
  }
27717
28013
  case "turn-marker": return textLines(` ⏹ ${entry.text}`, width, "dim");
27718
28014
  case "compaction": return textLines(entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${entry.error}`, width, "dim");
27719
28015
  case "retry": return textLines(` ↻ retry ${entry.attempt}/${entry.max} · ${entry.code} · ${Math.round(entry.delayMs / 100) / 10}s`, width, entry.state === "running" ? "warn" : "dim");
27720
- case "files": return entry.paths.length === 0 ? textLines(" ⎄ no changed files", width, "dim") : [...textLines(` ⎄ ${entry.paths.length} changed file${entry.paths.length === 1 ? "" : "s"}`, width, "dim"), ...entry.paths.flatMap((path) => textLines(` ${path}`, width, "dim"))];
28016
+ case "files": return entry.paths.length === 0 ? textLines(" ⎄ no changed files", width, "dim") : [...textLines(` ⎄ ${entry.paths.length} changed file${entry.paths.length === 1 ? "" : "s"}`, width, "dim"), ...entry.paths.flatMap((path) => hangingTextLines(path, width, " ", "dim", " "))];
27721
28017
  case "error": return textLines(entry.text, width, "error");
27722
28018
  default: return entry;
27723
28019
  }
27724
28020
  }
28021
+ /** Settled-history variant carrying the Ctrl+R reasoning fold. */
28022
+ function settledEntryLines(entry, columns, showReasoning) {
28023
+ return transcriptEntryLines(entry, columns, showReasoning, false);
28024
+ }
28025
+ //#endregion
28026
+ //#region src/render/editor.ts
28027
+ /**
28028
+ * Pure composer editor model with Codex textarea semantics: a grapheme
28029
+ * cursor over a column-safe multiline layout, word/piece motion, single-entry
28030
+ * kill + yank, and the shell-recall boundary gate that keeps Up/Down usable
28031
+ * inside a multiline draft.
28032
+ *
28033
+ * The model is intentionally string-offset based (UTF-16 indices clamped to
28034
+ * grapheme boundaries) so the React state stays two primitives
28035
+ * (value, cursor) and every operation here stays pure and testable.
28036
+ *
28037
+ * Word motion deviates from Codex's UAX#29 segmentation in one deliberate
28038
+ * way: a run of same-class characters is ONE piece, so a CJK run moves as a
28039
+ * single word (two hanzi are one Alt+B step, not two).
28040
+ *
28041
+ * @module @deepseek-ai/dsh-code/render/editor
28042
+ */
28043
+ const segmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter("en", { granularity: "grapheme" }) : void 0;
28044
+ /**
28045
+ * Split text into grapheme clusters. Falls back to code points when
28046
+ * Intl.Segmenter is unavailable; the fallback still keeps surrogate pairs
28047
+ * (emoji) atomic so the cursor can never split one.
28048
+ */
28049
+ function splitGraphemes(text) {
28050
+ if (text === "") return [];
28051
+ const spans = [];
28052
+ if (segmenter !== void 0) {
28053
+ for (const piece of segmenter.segment(text)) spans.push({
28054
+ text: piece.segment,
28055
+ start: piece.index,
28056
+ end: piece.index + piece.segment.length,
28057
+ width: visibleColumns(piece.segment)
28058
+ });
28059
+ return spans;
28060
+ }
28061
+ let start = 0;
28062
+ for (const char of text) {
28063
+ spans.push({
28064
+ text: char,
28065
+ start,
28066
+ end: start + char.length,
28067
+ width: visibleColumns(char)
28068
+ });
28069
+ start += char.length;
28070
+ }
28071
+ return spans;
28072
+ }
28073
+ /** Round one UTF-16 offset down to the grapheme boundary at or before it. */
28074
+ function floorBoundary(spans, offset) {
28075
+ for (let index = spans.length - 1; index >= 0; index -= 1) {
28076
+ const span = spans[index];
28077
+ if (span.end <= offset) return span.end;
28078
+ if (span.start < offset) return span.start;
28079
+ }
28080
+ return 0;
28081
+ }
28082
+ /** Clamp a cursor offset to the nearest grapheme boundary (surrogates, ZWJ, marks stay whole). */
28083
+ function clampCursor(value, offset) {
28084
+ if (value === "") return 0;
28085
+ const target = Math.max(0, Math.min(value.length, Math.floor(offset)));
28086
+ if (target === 0 || target === value.length) return target;
28087
+ const spans = splitGraphemes(value);
28088
+ const down = floorBoundary(spans, target);
28089
+ if (down === target) return target;
28090
+ const up = spans.find((span) => span.start >= target)?.start ?? value.length;
28091
+ return up - target < target - down ? up : down;
28092
+ }
28093
+ /**
28094
+ * Delete the final grapheme cluster (append-only drafts without a cursor).
28095
+ * Surrogate pairs and multi-codepoint emoji stay whole instead of leaving a
28096
+ * lone trailing code unit behind.
28097
+ */
28098
+ function deleteLastGrapheme(text) {
28099
+ if (text === "") return "";
28100
+ const spans = splitGraphemes(text);
28101
+ return text.slice(0, spans[spans.length - 1].start);
28102
+ }
28103
+ /**
28104
+ * Step the cursor by whole graphemes (negative steps left). The cursor is
28105
+ * assumed to sit on a boundary; any drift is clamped first.
28106
+ */
28107
+ function moveCursorBy(value, offset, delta) {
28108
+ if (delta === 0 || value === "") return clampCursor(value, offset);
28109
+ const spans = splitGraphemes(value);
28110
+ const boundaries = [0];
28111
+ for (const span of spans) boundaries.push(span.end);
28112
+ const current = boundaries.indexOf(clampCursor(value, offset));
28113
+ if (current === -1) return clampCursor(value, offset);
28114
+ return boundaries[Math.max(0, Math.min(boundaries.length - 1, current + delta))];
28115
+ }
28116
+ /**
28117
+ * Normalize text entering the draft: CRLF/CR become LF, tabs become two
28118
+ * spaces (terminal tab stops are contextual and cannot join a deterministic
28119
+ * row budget), and every other C0 control byte plus DEL is REMOVED — the
28120
+ * draft is data, so a stray ESC (Windows Terminal file drops) disappears
28121
+ * instead of rendering as literal backslash-x-1-b text. Newlines survive.
28122
+ */
28123
+ function sanitizeDraftText(text) {
28124
+ return text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "").replaceAll(" ", " ");
28125
+ }
28126
+ /**
28127
+ * Hard-wrap the draft into column-safe physical rows. Wide graphemes never
28128
+ * split across rows (a grapheme that does not fit flushes the row first) and
28129
+ * explicit newlines end their row without occupying a cell.
28130
+ */
28131
+ function editorModel(value, columns) {
28132
+ const width = Math.max(1, Math.floor(columns));
28133
+ const spans = splitGraphemes(value);
28134
+ const rows = [];
28135
+ let text = "";
28136
+ let start = 0;
28137
+ let used = 0;
28138
+ const offsets = [0];
28139
+ const rowColumns = [0];
28140
+ const rowCuts = [0];
28141
+ const flush = (end) => {
28142
+ rows.push({
28143
+ text,
28144
+ start,
28145
+ end,
28146
+ offsets: [...offsets],
28147
+ columns: [...rowColumns],
28148
+ cuts: [...rowCuts]
28149
+ });
28150
+ text = "";
28151
+ used = 0;
28152
+ offsets.length = 0;
28153
+ rowColumns.length = 0;
28154
+ rowCuts.length = 0;
28155
+ };
28156
+ for (const span of spans) {
28157
+ if (span.text === "\n") {
28158
+ flush(span.start);
28159
+ start = span.end;
28160
+ offsets.push(span.end);
28161
+ rowColumns.push(0);
28162
+ rowCuts.push(0);
28163
+ continue;
28164
+ }
28165
+ if (used > 0 && used + span.width > width) {
28166
+ flush(span.start);
28167
+ start = span.start;
28168
+ offsets.push(span.start);
28169
+ rowColumns.push(0);
28170
+ rowCuts.push(0);
28171
+ }
28172
+ text += span.text;
28173
+ used += span.width;
28174
+ offsets.push(span.end);
28175
+ rowColumns.push(used);
28176
+ rowCuts.push(text.length);
28177
+ }
28178
+ if (text !== "" || rows.length === 0) flush(value.length);
28179
+ else if (offsets.length > 0) rows.push({
28180
+ text: "",
28181
+ start,
28182
+ end: value.length,
28183
+ offsets: [...offsets],
28184
+ columns: [...rowColumns],
28185
+ cuts: [...rowCuts]
28186
+ });
28187
+ return {
28188
+ rows,
28189
+ length: value.length
28190
+ };
28191
+ }
28192
+ /** Map a cursor offset to its caret site on the wrapped rows. */
28193
+ function caretSite(model, offset) {
28194
+ const target = Math.max(0, Math.min(model.length, offset));
28195
+ for (let index = model.rows.length - 1; index >= 0; index -= 1) {
28196
+ const row = model.rows[index];
28197
+ const at = row.offsets.indexOf(target);
28198
+ if (at >= 0) return {
28199
+ row: index,
28200
+ column: row.columns[at]
28201
+ };
28202
+ }
28203
+ const last = model.rows[model.rows.length - 1];
28204
+ return {
28205
+ row: model.rows.length - 1,
28206
+ column: last === void 0 ? 0 : last.columns[last.columns.length - 1]
28207
+ };
28208
+ }
28209
+ /**
28210
+ * Move the caret across physical rows keeping a preferred display column
28211
+ * (Codex `preferred_col`): horizontal moves reset the preference, vertical
28212
+ * moves reuse it, clamped to each row's width.
28213
+ */
28214
+ function moveCursorVertically(model, offset, preferredColumn, delta) {
28215
+ const target = caretSite(model, offset).row + delta;
28216
+ if (target < 0 || target >= model.rows.length || delta === 0) return offset;
28217
+ const row = model.rows[target];
28218
+ const wanted = Math.max(0, Math.min(preferredColumn, row.columns[row.columns.length - 1]));
28219
+ let best = 0;
28220
+ for (let index = 1; index < row.columns.length; index += 1) if (row.columns[index] <= wanted) best = index;
28221
+ else break;
28222
+ return row.offsets[best];
28223
+ }
28224
+ /** The start/end offsets of the logical line containing the cursor. */
28225
+ function lineBounds(value, offset) {
28226
+ const target = Math.max(0, Math.min(value.length, offset));
28227
+ const start = value.lastIndexOf("\n", Math.max(0, target - 1)) + 1;
28228
+ const end = value.indexOf("\n", target);
28229
+ return {
28230
+ start,
28231
+ end: end === -1 ? value.length : end
28232
+ };
28233
+ }
28234
+ /** Codex WORD_SEPARATORS: punctuation runs are their own word pieces. */
28235
+ const WORD_SEPARATORS = /* @__PURE__ */ new Set("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?");
28236
+ function classifyGrapheme(text) {
28237
+ if (/^\s$/u.test(text)) return "space";
28238
+ return WORD_SEPARATORS.has(text) ? "punct" : "word";
28239
+ }
28240
+ /** Maximal same-class runs of graphemes as [start, end) spans. */
28241
+ function pieceRuns(value) {
28242
+ const runs = [];
28243
+ let current;
28244
+ for (const span of splitGraphemes(value)) {
28245
+ const klass = span.text === "\n" ? "space" : classifyGrapheme(span.text);
28246
+ if (current !== void 0 && current.class === klass) {
28247
+ current.end = span.end;
28248
+ continue;
28249
+ }
28250
+ current = {
28251
+ start: span.start,
28252
+ end: span.end,
28253
+ class: klass
28254
+ };
28255
+ runs.push(current);
28256
+ }
28257
+ return runs;
28258
+ }
28259
+ /**
28260
+ * Codex `beginning_of_previous_word`: skip whitespace left, then land on the
28261
+ * START of the trailing non-space piece (extending over separator pieces).
28262
+ */
28263
+ function moveWordLeft(value, offset) {
28264
+ const cursor = Math.max(0, Math.min(value.length, offset));
28265
+ const runs = pieceRuns(value);
28266
+ let index = runs.length - 1;
28267
+ while (index >= 0 && runs[index].end > cursor) index -= 1;
28268
+ if (index < 0) return 0;
28269
+ if (runs[index].class === "space") {
28270
+ index -= 1;
28271
+ if (index < 0) return 0;
28272
+ }
28273
+ let target = index;
28274
+ while (target > 0 && runs[target].class === "punct" && runs[target - 1].class === "punct") target -= 1;
28275
+ return runs[target].start;
28276
+ }
28277
+ /**
28278
+ * Codex `end_of_next_word`: skip whitespace right, then land on the END of
28279
+ * the leading non-space piece (extending over separator pieces).
28280
+ */
28281
+ function moveWordRight(value, offset) {
28282
+ const cursor = Math.max(0, Math.min(value.length, offset));
28283
+ const runs = pieceRuns(value);
28284
+ let index = 0;
28285
+ while (index < runs.length && runs[index].start < cursor) index += 1;
28286
+ if (index >= runs.length) return value.length;
28287
+ if (runs[index].class === "space") {
28288
+ index += 1;
28289
+ if (index >= runs.length) return value.length;
28290
+ }
28291
+ let target = index;
28292
+ while (target < runs.length - 1 && runs[target].class === "punct" && runs[target + 1].class === "punct") target += 1;
28293
+ return runs[target].end;
28294
+ }
28295
+ function replaceRange(value, cursor, start, end, killed) {
28296
+ const from = Math.min(start, end);
28297
+ const to = Math.max(start, end);
28298
+ if (from >= to) return {
28299
+ value,
28300
+ cursor,
28301
+ killed: void 0
28302
+ };
28303
+ const text = value.slice(from, to);
28304
+ return {
28305
+ value: value.slice(0, from) + value.slice(to),
28306
+ cursor: Math.max(0, cursor - Math.max(0, Math.min(cursor, to) - from)),
28307
+ killed: killed ? text : void 0
28308
+ };
28309
+ }
28310
+ /** Delete the grapheme cluster before the cursor. */
28311
+ function deleteBackward(value, cursor) {
28312
+ if (cursor === 0) return {
28313
+ value,
28314
+ cursor,
28315
+ killed: void 0
28316
+ };
28317
+ const spans = splitGraphemes(value.slice(0, cursor));
28318
+ return replaceRange(value, cursor, spans.length === 0 ? 0 : spans[spans.length - 1].start, cursor, false);
28319
+ }
28320
+ /** Delete the grapheme cluster at the cursor. */
28321
+ function deleteForward(value, cursor) {
28322
+ if (cursor >= value.length) return {
28323
+ value,
28324
+ cursor,
28325
+ killed: void 0
28326
+ };
28327
+ const span = splitGraphemes(value).find((candidate) => candidate.start >= cursor);
28328
+ return replaceRange(value, cursor, cursor, span === void 0 ? value.length : span.end, false);
28329
+ }
28330
+ /** Delete back to the start of the previous word (fills the kill buffer). */
28331
+ function deleteWordBackward(value, cursor) {
28332
+ return replaceRange(value, cursor, moveWordLeft(value, cursor), cursor, true);
28333
+ }
28334
+ /** Delete forward to the end of the next word (fills the kill buffer). */
28335
+ function deleteWordForward(value, cursor) {
28336
+ return replaceRange(value, cursor, cursor, moveWordRight(value, cursor), true);
28337
+ }
28338
+ /** Ctrl+U: kill from the line start to the cursor; at BOL, kill the newline. */
28339
+ function killToLineStart(value, cursor) {
28340
+ const bounds = lineBounds(value, cursor);
28341
+ if (cursor > bounds.start) return replaceRange(value, cursor, bounds.start, cursor, true);
28342
+ if (bounds.start > 0) return replaceRange(value, cursor, bounds.start - 1, bounds.start, true);
28343
+ return {
28344
+ value,
28345
+ cursor,
28346
+ killed: void 0
28347
+ };
28348
+ }
28349
+ /** Ctrl+K: kill from the cursor to the line end; at EOL, kill the newline. */
28350
+ function killToLineEnd(value, cursor) {
28351
+ const bounds = lineBounds(value, cursor);
28352
+ if (cursor < bounds.end) return replaceRange(value, cursor, cursor, bounds.end, true);
28353
+ if (bounds.end < value.length) return replaceRange(value, cursor, bounds.end, bounds.end + 1, true);
28354
+ return {
28355
+ value,
28356
+ cursor,
28357
+ killed: void 0
28358
+ };
28359
+ }
28360
+ /** Insert sanitized text at the cursor. */
28361
+ function insertText(value, cursor, text) {
28362
+ const safe = sanitizeDraftText(text);
28363
+ if (safe === "") return {
28364
+ value,
28365
+ cursor,
28366
+ killed: void 0
28367
+ };
28368
+ return {
28369
+ value: value.slice(0, cursor) + safe + value.slice(cursor),
28370
+ cursor: cursor + safe.length,
28371
+ killed: void 0
28372
+ };
28373
+ }
28374
+ /**
28375
+ * Composer editor row budget: the editor itself never grows past this many
28376
+ * physical rows; deeper drafts scroll internally to keep the caret visible.
28377
+ * Short terminals collapse toward one row so the live transcript keeps room.
28378
+ */
28379
+ function composerMaxRows(terminalRows) {
28380
+ return Math.max(1, Math.min(6, Math.floor((Math.max(1, terminalRows) - 10) / 3)));
28381
+ }
28382
+ /**
28383
+ * Codex `should_handle_navigation`: Up/Down walk history only from an empty
28384
+ * draft, or from a boundary of a draft that still exactly matches the last
28385
+ * recalled entry. Any interior cursor position keeps vertical caret movement.
28386
+ */
28387
+ function shouldRecallNavigate(value, cursor, lastRecalled) {
28388
+ if (value === "") return true;
28389
+ if (cursor !== 0 && cursor !== value.length) return false;
28390
+ return lastRecalled === value;
28391
+ }
28392
+ /** Bracketed paste markers as Ink delivers them (it strips the leading ESC). */
28393
+ const PASTE_START_MARKER = "[200~";
28394
+ const PASTE_END_MARKER = "[201~";
28395
+ /**
28396
+ * Remove bracketed paste markers from one input chunk. Panel drafts accept raw
28397
+ * `input` text, where an unhandled paste would otherwise persist the literal
28398
+ * "[200~"/"[201~" markers Ink leaves after stripping the ESC byte.
28399
+ */
28400
+ function stripPasteMarkers(text) {
28401
+ return text.replaceAll(PASTE_START_MARKER, "").replaceAll(PASTE_END_MARKER, "");
28402
+ }
28403
+ /** Match one CSI-u sequence (code, optional ;modifiers, then :event or ;alternate). */
28404
+ const CSI_U_SOURCE = "\x1B\\[(\\d+)(?:;(\\d+))?(?:[:;](\\d+))?u";
28405
+ /** Legacy equivalent for one decoded CSI-u key, or undefined to pass through. */
28406
+ function legacyForKey(key) {
28407
+ const bits = Math.max(0, key.modifiers - 1);
28408
+ const shift = (bits & 1) !== 0;
28409
+ const alt = (bits & 2) !== 0;
28410
+ const ctrl = (bits & 4) !== 0;
28411
+ if (key.code === 13) {
28412
+ if (shift) return "\x1B[13;2u";
28413
+ if (ctrl) return "\n";
28414
+ if (alt) return "\x1B\r";
28415
+ return "\r";
28416
+ }
28417
+ if (key.code === 27) return "\x1B";
28418
+ if (key.code === 9) return shift ? "\x1B[Z" : " ";
28419
+ if (key.code === 127) return alt || ctrl ? "\x1B" : "";
28420
+ if (key.code >= 1 && key.code <= 6) {
28421
+ const mask = (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
28422
+ const mods = mask === 0 ? "" : `;${mask + 1}`;
28423
+ if (key.code === 1) return mods === "" ? "\x1B[H" : `\x1b[1${mods}H`;
28424
+ if (key.code === 4) return mods === "" ? "\x1B[F" : `\x1b[1${mods}F`;
28425
+ return `\x1b[${key.code}${mods}~`;
28426
+ }
28427
+ if (key.code >= 97 && key.code <= 122) {
28428
+ const letter = String.fromCodePoint(key.code);
28429
+ if (ctrl) return String.fromCodePoint(key.code - 96);
28430
+ if (alt) return "\x1B" + letter;
28431
+ if (shift) return String.fromCodePoint(key.alternate ?? key.code - 32);
28432
+ return letter;
28433
+ }
28434
+ if (key.code >= 65 && key.code <= 90) {
28435
+ if (ctrl) return String.fromCodePoint(key.code + 32 - 96);
28436
+ if (alt) return "\x1B" + String.fromCodePoint(key.code + 32);
28437
+ return String.fromCodePoint(key.code);
28438
+ }
28439
+ if (key.code >= 32 && key.code <= 126 && key.alternate !== void 0) {
28440
+ const base = key.alternate >= 97 && key.alternate <= 122 ? key.alternate : key.code;
28441
+ if (ctrl && base - 96 >= 1 && base - 96 <= 26) return String.fromCodePoint(base - 96);
28442
+ if (alt) return "\x1B" + String.fromCodePoint(key.alternate);
28443
+ return String.fromCodePoint(key.alternate);
28444
+ }
28445
+ }
28446
+ /**
28447
+ * Rewrite every decodable kitty CSI-u sequence in one stdin chunk to the
28448
+ * legacy form the input layer already handles. Undecodable or non-key
28449
+ * sequences pass through untouched, so terminals without the protocol are
28450
+ * unaffected.
28451
+ */
28452
+ function normalizeKeyboardChunk(chunk) {
28453
+ if (!chunk.includes("\x1B[") || !chunk.includes("u")) return chunk;
28454
+ const pattern = new RegExp(CSI_U_SOURCE, "g");
28455
+ return chunk.replace(pattern, (whole, code, mods, third) => {
28456
+ return legacyForKey({
28457
+ code: Number.parseInt(code, 10),
28458
+ modifiers: mods === void 0 || mods === "" ? 1 : Math.max(1, Number.parseInt(mods, 10)),
28459
+ alternate: third !== void 0 && third !== "" ? Number.parseInt(third, 10) : void 0
28460
+ }) ?? whole;
28461
+ });
28462
+ }
27725
28463
  //#endregion
27726
28464
  //#region src/kernel-panels.ts
27727
28465
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
@@ -27780,9 +28518,15 @@ function ListFrame(props) {
27780
28518
  wrap: "truncate-end"
27781
28519
  }, truncateColumns(singleLineText(props.footer), viewport.contentColumns)));
27782
28520
  }
28521
+ /**
28522
+ * Apply one keystroke to a panel search query. IME commits arrive as one
28523
+ * multi-character chunk, so the whole printable run is appended; paste
28524
+ * markers are stripped and control-laden chunks are ignored.
28525
+ */
27783
28526
  function editQuery(query, input, key) {
27784
- if (key.backspace || key.delete) return query.slice(0, -1);
27785
- if (input.length === 1 && input >= " " && input !== "") return query + input;
28527
+ if (key.backspace || key.delete) return deleteLastGrapheme(query);
28528
+ const text = stripPasteMarkers(input);
28529
+ if (text !== "" && !/[\u0000-\u001f\u007f]/u.test(text)) return query + text;
27786
28530
  }
27787
28531
  function ModePanel({ current, load, select, close }) {
27788
28532
  const [rows, setRows] = (0, import_react.useState)([]);
@@ -27907,6 +28651,57 @@ function PluginPanel({ load, close, initialQuery = "" }) {
27907
28651
  footer: "↑↓ inspect · enter details · r refresh · esc close"
27908
28652
  });
27909
28653
  }
28654
+ /** Web TurnStatus elapsed format: `45s` under a minute, `2m03s` beyond. */
28655
+ function runClock(ms) {
28656
+ const total = Math.max(0, Math.floor(ms / 1e3));
28657
+ const minutes = Math.floor(total / 60);
28658
+ const seconds = total % 60;
28659
+ return minutes > 0 ? `${minutes}m${String(seconds).padStart(2, "0")}s` : `${seconds}s`;
28660
+ }
28661
+ /** Status glyph per job lifecycle state. */
28662
+ const JOB_MARK = {
28663
+ running: "●",
28664
+ stopping: "⏸",
28665
+ completed: "✓",
28666
+ killed: "⊘",
28667
+ failed: "✗"
28668
+ };
28669
+ /**
28670
+ * The read-only background-job panel: caller-owned and unowned jobs from the
28671
+ * host `jobs` registry in registration order, with a local second-hand while
28672
+ * the panel is open (elapsed clocks advance and the snapshot re-reads; the
28673
+ * interval dies with the panel). Cancel stays upstream-only; an absent
28674
+ * registry renders as the plain empty state (a harmless missing service).
28675
+ */
28676
+ function JobsPanel({ load, close }) {
28677
+ const [, setRefresh] = (0, import_react.useState)(0);
28678
+ const [cursor, setCursor] = (0, import_react.useState)(0);
28679
+ const [, setTick] = (0, import_react.useState)(0);
28680
+ (0, import_react.useEffect)(() => {
28681
+ const id = setInterval(() => setTick((value) => value + 1), 1e3);
28682
+ return () => clearInterval(id);
28683
+ }, []);
28684
+ const rows = load();
28685
+ useInput((input, key) => {
28686
+ if (key.escape || input === "q") return close();
28687
+ if (input === "r") return setRefresh((value) => value + 1);
28688
+ if (key.upArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + rows.length - 1) % rows.length);
28689
+ if (key.downArrow) return setCursor((value) => rows.length === 0 ? 0 : (value + 1) % rows.length);
28690
+ });
28691
+ (0, import_react.useEffect)(() => setCursor((value) => Math.min(value, Math.max(0, rows.length - 1))), [rows.length]);
28692
+ return (0, import_react.createElement)(ListFrame, {
28693
+ title: `/jobs · background tasks · ${rows.length}`,
28694
+ rows: rows.map((row) => ({
28695
+ key: row.id,
28696
+ text: `${JOB_MARK[row.status]} ${row.id} · ${singleLineText(row.label)} · ${runClock((row.finishedAt ?? Date.now()) - row.startedAt)}${row.detail === void 0 ? "" : ` · ${singleLineText(row.detail)}`}`
28697
+ })),
28698
+ cursor,
28699
+ loading: false,
28700
+ query: "",
28701
+ searching: false,
28702
+ footer: "↑↓ inspect · r refresh · esc close"
28703
+ });
28704
+ }
27910
28705
  function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }) {
27911
28706
  const [options, setOptions] = (0, import_react.useState)({
27912
28707
  sessions: "roots",
@@ -28054,11 +28849,42 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
28054
28849
  footer: "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume"
28055
28850
  });
28056
28851
  }
28852
+ function documentStyleProps(style) {
28853
+ switch (style) {
28854
+ case "accent": return { color: inkColor(getPalette().brandBright) };
28855
+ case "accentBold": return {
28856
+ color: inkColor(getPalette().brandBright),
28857
+ bold: true
28858
+ };
28859
+ case "code": return { color: inkColor(getPalette().code) };
28860
+ case "dim": return { color: inkColor(getPalette().dim) };
28861
+ case "bold": return { bold: true };
28862
+ case "italic": return { italic: true };
28863
+ case "boldItalic": return {
28864
+ bold: true,
28865
+ italic: true
28866
+ };
28867
+ case "strike": return {
28868
+ color: inkColor(getPalette().dim),
28869
+ strikethrough: true
28870
+ };
28871
+ default: return {};
28872
+ }
28873
+ }
28874
+ function DocumentRows({ lines }) {
28875
+ return (0, import_react.createElement)(Box, { flexDirection: "column" }, ...lines.map((line, index) => (0, import_react.createElement)(Text, {
28876
+ key: index,
28877
+ wrap: "truncate-end"
28878
+ }, line.segments.length === 0 ? " " : line.segments.map((segment, at) => (0, import_react.createElement)(Text, {
28879
+ key: at,
28880
+ ...documentStyleProps(segment.style)
28881
+ }, segment.text)))));
28882
+ }
28057
28883
  function DocumentPanel({ title, text, error, close }) {
28058
28884
  const stdout = useStdout().stdout;
28059
28885
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
28060
28886
  const [scroll, setScroll] = (0, import_react.useState)(0);
28061
- const lines = (0, import_react.useMemo)(() => text === void 0 ? [] : textLines(text, viewport.contentColumns).map((line) => line.segments.map((segment) => segment.text).join("")), [text, viewport.contentColumns]);
28887
+ const lines = (0, import_react.useMemo)(() => text === void 0 ? [] : markdownLines(text, viewport.contentColumns), [text, viewport.contentColumns]);
28062
28888
  useInput((input, key) => {
28063
28889
  if (key.escape || input === "q" || input === "t") return close();
28064
28890
  if (key.upArrow) return setScroll((value) => Math.max(0, value - 1));
@@ -28069,7 +28895,7 @@ function DocumentPanel({ title, text, error, close }) {
28069
28895
  if (input === "G") return setScroll(Math.max(0, lines.length - viewport.bodyRows));
28070
28896
  });
28071
28897
  if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("transcript · esc close", viewport.contentColumns));
28072
- const body = error !== void 0 ? [`error: ${singleLineText(error)}`] : text === void 0 ? ["loading transcript…"] : lines.slice(scroll, scroll + viewport.bodyRows);
28898
+ const body = error !== void 0 ? textLines(`error: ${singleLineText(error)}`, viewport.contentColumns, "error") : text === void 0 ? textLines("loading transcript…", viewport.contentColumns, "dim") : lines.slice(scroll, scroll + viewport.bodyRows);
28073
28899
  return (0, import_react.createElement)(Box, {
28074
28900
  width: viewport.outerColumns,
28075
28901
  borderStyle: "round",
@@ -28079,10 +28905,7 @@ function DocumentPanel({ title, text, error, close }) {
28079
28905
  }, (0, import_react.createElement)(Text, {
28080
28906
  color: inkColor(getPalette().brandBright),
28081
28907
  wrap: "truncate-end"
28082
- }, truncateColumns(singleLineText(title), viewport.contentColumns)), ...body.map((line, index) => (0, import_react.createElement)(Text, {
28083
- key: `${scroll}-${index}`,
28084
- wrap: "truncate-end"
28085
- }, truncateColumns(line, viewport.contentColumns))), (0, import_react.createElement)(Text, {
28908
+ }, truncateColumns(singleLineText(title), viewport.contentColumns)), (0, import_react.createElement)(DocumentRows, { lines: body }), (0, import_react.createElement)(Text, {
28086
28909
  dimColor: true,
28087
28910
  wrap: "truncate-end"
28088
28911
  }, truncateColumns(`lines ${lines.length === 0 ? 0 : scroll + 1}-${Math.min(lines.length, scroll + viewport.bodyRows)}/${lines.length} · ↑↓/pg/g/G · t/esc close`, viewport.contentColumns)));
@@ -28109,13 +28932,16 @@ function HistoryPanel({ entries, fill, close }) {
28109
28932
  if (input === "g") return setCursor(0);
28110
28933
  if (input === "G") return setCursor(matches.length - 1);
28111
28934
  if (key.backspace) {
28112
- setQuery((current) => current.slice(0, -1));
28935
+ setQuery((current) => deleteLastGrapheme(current));
28113
28936
  setCursor(0);
28114
28937
  return;
28115
28938
  }
28116
28939
  if (input !== "" && !key.ctrl && !key.meta && !key.shift) {
28117
- setQuery((current) => (current + input).slice(0, 120));
28118
- setCursor(0);
28940
+ const text = stripPasteMarkers(input);
28941
+ if (text !== "") {
28942
+ setQuery((current) => (current + text).slice(0, 120));
28943
+ setCursor(0);
28944
+ }
28119
28945
  }
28120
28946
  });
28121
28947
  const stdout = useStdout().stdout;
@@ -28546,6 +29372,15 @@ function recallEntries(persistent, local) {
28546
29372
  const localSet = new Set(local);
28547
29373
  return [...persistent.filter((entry) => !localSet.has(entry)), ...local].reverse();
28548
29374
  }
29375
+ /** Fresh navigation state over one recall space. */
29376
+ function beginRecall(entries, draft) {
29377
+ return {
29378
+ entries,
29379
+ index: null,
29380
+ savedDraft: draft,
29381
+ lastRecalled: null
29382
+ };
29383
+ }
28549
29384
  /**
28550
29385
  * Move one entry older (Up, toward index +1 in the newest-first space). The
28551
29386
  * first Up saves the current draft so Down past the newest can restore it
@@ -28629,8 +29464,32 @@ function recallNewer(state) {
28629
29464
  */
28630
29465
  /** Match Codex's settled-resize window before rebuilding terminal scrollback. */
28631
29466
  const RESIZE_REFLOW_DELAY_MS = 75;
28632
- /** Reset region/style, clear the visible screen and scrollback, then home. */
28633
- const RESIZE_REFLOW_CLEAR = "\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H";
29467
+ /**
29468
+ * Cap on rendered settled history, in physical rows (header and hint
29469
+ * included). 3,000 rows sits inside Codex's 1k–10k reflow budget range:
29470
+ * replays stay under ~200ms while roughly a hundred messages stay visible
29471
+ * before the oldest drop out. `DSH_SETTLED_ROWS` overrides it; 0 disables
29472
+ * the cap entirely (the historical unbounded behavior).
29473
+ */
29474
+ const SETTLED_ROW_CAP = readSettledRowCap();
29475
+ /** Hysteresis: the cap may overflow by 25% before one trimming replay fires. */
29476
+ /** Header rows plus the trim hint, reserved out of the row cap. */
29477
+ const SETTLED_ROW_RESERVE = 12;
29478
+ /** Read the configurable settled-history cap once per process. */
29479
+ function readSettledRowCap() {
29480
+ const raw = process.env.DSH_SETTLED_ROWS;
29481
+ if (raw === void 0) return 3e3;
29482
+ const parsed = Number.parseInt(raw, 10);
29483
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 3e3;
29484
+ }
29485
+ /**
29486
+ * Safety net for a bracketed paste whose end marker never arrives (terminal
29487
+ * defect or crash mid-paste): past this window the open-paste flag resets so
29488
+ * Enter submits again instead of inserting newlines forever.
29489
+ */
29490
+ const PASTE_BRACKET_TIMEOUT_MS = 1e3;
29491
+ /** Release the held frame after Ink has replayed the source-backed Static rows. */
29492
+ const SYNCHRONIZED_UPDATE_END = "\x1B[?2026l";
28634
29493
  /** Pad text with spaces to a visible-column target (menu name column). */
28635
29494
  function padColumns(text, width) {
28636
29495
  const clipped = truncateColumns(singleLineText(text), width);
@@ -28661,11 +29520,6 @@ function useStableInput(handler, active) {
28661
29520
  }, []);
28662
29521
  useInput(stableHandler, { isActive: active });
28663
29522
  }
28664
- /** Single-cell stepped pulse: the web's 125ms flat-hold brightness steps over 1s. */
28665
- function Pulse() {
28666
- const tick = useFrames(125);
28667
- return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, pulseFrame(tick));
28668
- }
28669
29523
  /**
28670
29524
  * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
28671
29525
  * ring trail clockwise around the eight outer positions (8 frames × 125ms =
@@ -28686,13 +29540,6 @@ function CursorBlock({ char }) {
28686
29540
  const tick = useFrames(530);
28687
29541
  return (0, import_react.createElement)(Text, { inverse: caretVisible(tick) || void 0 }, char);
28688
29542
  }
28689
- /** Web TurnStatus elapsed format: `45s` under a minute, `2m03s` beyond. */
28690
- function runClock(ms) {
28691
- const total = Math.max(0, Math.floor(ms / 1e3));
28692
- const minutes = Math.floor(total / 60);
28693
- const seconds = total % 60;
28694
- return minutes > 0 ? `${minutes}m${String(seconds).padStart(2, "0")}s` : `${seconds}s`;
28695
- }
28696
29543
  /**
28697
29544
  * The busy line, web TurnStatus contract: the StateDot chase leads the plain
28698
29545
  * `Deep diving...` label, with the elapsed clock appended only once the turn
@@ -28716,7 +29563,7 @@ function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = pref
28716
29563
  const columns = useStdout().stdout?.columns ?? 80;
28717
29564
  const safeRows = Math.max(1, maxRows);
28718
29565
  const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix));
28719
- const contentColumns = Math.max(10, columns - 3 - prefixColumns);
29566
+ const contentColumns = Math.max(10, columns - 1 - prefixColumns);
28720
29567
  const initial = displayTail(text, contentColumns, safeRows);
28721
29568
  const tail = initial.truncated && safeRows > 1 ? displayTail(text, contentColumns, safeRows - 1) : initial;
28722
29569
  const rows = tail.text.split("\n");
@@ -28838,138 +29685,80 @@ function StyledRows({ lines }) {
28838
29685
  ...lineStyleProps(segment.style)
28839
29686
  }, segment.text)))));
28840
29687
  }
28841
- /** Codex-style panel rhythm that still participates in the row budget. */
28842
- function PanelGap({ visible }) {
28843
- return visible ? (0, import_react.createElement)(Text, null, " ") : void 0;
28844
- }
28845
- /** One settled markdown document rendered as styled lines at the terminal width. */
28846
- function MarkdownBody({ text, indent = 0 }) {
28847
- const columns = useStdout().stdout?.columns ?? 80;
28848
- const lines = (0, import_react.useMemo)(() => renderMarkdown(displayText(text), Math.max(20, columns - 2 - indent)), [
28849
- text,
28850
- columns,
28851
- indent
28852
- ]);
29688
+ /** File-oriented, color-coded unified diff viewport. */
29689
+ function DiffPanel({ view, onClose }) {
29690
+ const stdout = useStdout().stdout;
29691
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29692
+ const [fileIndex, setFileIndex] = (0, import_react.useState)(0);
29693
+ const [scroll, setScroll] = (0, import_react.useState)(0);
29694
+ const file = view.files[fileIndex];
29695
+ const lines = (0, import_react.useMemo)(() => {
29696
+ if (file === void 0) return textLines(" (no changes)", viewport.contentColumns, "dim");
29697
+ return file.lines.flatMap((line) => styledLines([lineSegment(line, line.startsWith("+") && !line.startsWith("+++") ? "success" : line.startsWith("-") && !line.startsWith("---") ? "error" : line.startsWith("@@") || line.startsWith("diff --git") || line.startsWith("index ") ? "brand" : "dim")], viewport.contentColumns));
29698
+ }, [file, viewport.contentColumns]);
29699
+ const visibleScroll = clampScroll(scroll, lines.length, viewport.bodyRows);
29700
+ useInput((input, key) => {
29701
+ if (key.escape || input === "q") onClose();
29702
+ else if (key.leftArrow && view.files.length > 0) {
29703
+ setFileIndex((current) => (current + view.files.length - 1) % view.files.length);
29704
+ setScroll(0);
29705
+ } else if (key.rightArrow && view.files.length > 0) {
29706
+ setFileIndex((current) => (current + 1) % view.files.length);
29707
+ setScroll(0);
29708
+ } else if (input === "g") setScroll(0);
29709
+ else if (input === "G") setScroll(Math.max(0, lines.length - viewport.bodyRows));
29710
+ else if (key.upArrow) setScroll((current) => moveScroll(current, -1, lines.length, viewport.bodyRows));
29711
+ else if (key.downArrow) setScroll((current) => moveScroll(current, 1, lines.length, viewport.bodyRows));
29712
+ else if (key.pageUp) setScroll((current) => moveScroll(current, -viewport.bodyRows, lines.length, viewport.bodyRows));
29713
+ else if (key.pageDown) setScroll((current) => moveScroll(current, viewport.bodyRows, lines.length, viewport.bodyRows));
29714
+ });
29715
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns(`${view.title} · ${view.files.length} files · esc/q close`, viewport.contentColumns));
28853
29716
  return (0, import_react.createElement)(Box, {
28854
29717
  flexDirection: "column",
28855
- paddingLeft: indent
28856
- }, ...lines.map((line, index) => (0, import_react.createElement)(Text, { key: index }, line.segments.length === 0 ? " " : line.segments.map((segment, at) => (0, import_react.createElement)(Text, {
28857
- key: at,
28858
- ...segmentProps(segment.style)
28859
- }, segment.text)))));
28860
- }
28861
- /** Expanded reasoning with the same two-column content edge as the reply. */
28862
- function ReasoningBody({ text }) {
28863
- const columns = useStdout().stdout?.columns ?? 80;
28864
- const lines = (0, import_react.useMemo)(() => reasoningLines(text, Math.max(10, columns - 2)), [text, columns]);
28865
- return (0, import_react.createElement)(StyledRows, { lines });
28866
- }
28867
- /**
28868
- * One expanded tool-card body for the verbose transcript (Ctrl+O): the
28869
- * presentation contract's structured cards — inline diffs, read windows,
28870
- * web sources — rendered as plain terminal rows, degradation-safe against
28871
- * replayed metadata.
28872
- */
28873
- function ToolDetailBody({ detail }) {
28874
- switch (detail.kind) {
28875
- case "diff": return (0, import_react.createElement)(Box, { flexDirection: "column" }, ...detail.diffs.map((diff, index) => (0, import_react.createElement)(Box, {
28876
- key: index,
28877
- flexDirection: "column"
28878
- }, (0, import_react.createElement)(Text, {
28879
- dimColor: true,
28880
- wrap: "truncate-end"
28881
- }, ` ── ${displayText(diff.path)}${diff.truncated ? " (diff truncated)" : ""}`), ...diff.lines.map((line, at) => (0, import_react.createElement)(Text, {
28882
- key: at,
28883
- color: line.mark === "+" ? inkColor(getPalette().success) : line.mark === "-" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
28884
- wrap: "truncate-end"
28885
- }, ` ${line.mark}${displayText(line.text)}`)))));
28886
- case "read": return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, {
28887
- dimColor: true,
28888
- wrap: "truncate-end"
28889
- }, ` ── ${displayText(detail.path)} · lines ${detail.offset}-${detail.lines.length > 0 ? detail.lines[detail.lines.length - 1].number : detail.offset - 1} of ${detail.totalLines}${detail.truncated ? " (window truncated)" : ""}`), ...detail.lines.map((line, at) => (0, import_react.createElement)(Text, {
28890
- key: at,
28891
- dimColor: true,
28892
- wrap: "truncate-end"
28893
- }, ` ${String(line.number).padStart(5, " ")} | ${displayText(line.text)}`)));
28894
- case "web-search": return (0, import_react.createElement)(Box, { flexDirection: "column" }, ...detail.sources.map((source, at) => (0, import_react.createElement)(Text, {
28895
- key: at,
28896
- wrap: "truncate-end"
28897
- }, brand(` ? ${displayText(source.title === void 0 ? source.url : source.title)}`), (0, import_react.createElement)(Text, { dimColor: true }, dim(` - ${displayText(source.url)}`)))), (0, import_react.createElement)(Text, { dimColor: true }, dim(` ${detail.sources.length} sources${detail.truncated ? " (capped)" : ""}`)));
28898
- case "web-fetch": return (0, import_react.createElement)(Text, {
28899
- dimColor: true,
28900
- wrap: "truncate-end"
28901
- }, dim(` ${displayText(detail.url)} · HTTP ${detail.statusCode}`));
28902
- case "raw": return (0, import_react.createElement)(Box, { flexDirection: "column" }, ...displayText(detail.text).split("\n").slice(0, 40).map((line, at) => (0, import_react.createElement)(Text, {
28903
- key: at,
28904
- dimColor: true,
28905
- wrap: "truncate-end"
28906
- }, ` ${line}`)), (0, import_react.createElement)(Text, { dimColor: true }, detail.truncated ? " … (output truncated)" : " (end of output)"));
28907
- default: return assertNever(detail, "tool detail kind");
28908
- }
29718
+ borderStyle: "round",
29719
+ borderColor: inkColor(getPalette().dim),
29720
+ paddingX: 1
29721
+ }, (0, import_react.createElement)(Text, {
29722
+ color: inkColor(getPalette().brand),
29723
+ bold: true,
29724
+ wrap: "truncate-end"
29725
+ }, truncateColumns(`${view.title} · ${view.files.length === 0 ? "no files" : `${fileIndex + 1}/${view.files.length} ${file?.path ?? ""}`} · rows ${lines.length === 0 ? 0 : visibleScroll + 1}-${Math.min(lines.length, visibleScroll + viewport.bodyRows)}/${lines.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(StyledRows, { lines: lines.slice(visibleScroll, visibleScroll + viewport.bodyRows) }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
29726
+ color: inkColor(getPalette().dim),
29727
+ wrap: "truncate-end"
29728
+ }, truncateColumns("↑/↓ scroll · g/G ends · esc/q close", viewport.contentColumns)));
28909
29729
  }
28910
- /** One settled transcript row. */
28911
- function EntryLine({ entry, showReasoning, verbose }) {
28912
- switch (entry.kind) {
28913
- case "user": return entry.notice ? (0, import_react.createElement)(Text, { dimColor: true }, `⤷ ${displayText(entry.text)}`) : (0, import_react.createElement)(Text, null, brand("❯ "), displayText(entry.text));
28914
- case "assistant": return (0, import_react.createElement)(Box, { flexDirection: "column" }, entry.reasoning === "" ? void 0 : showReasoning ? (0, import_react.createElement)(ReasoningBody, { text: entry.reasoning }) : (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, `✻ Thinking (${entry.reasoning.length} chars, Ctrl+R to expand)`), (0, import_react.createElement)(MarkdownBody, {
28915
- text: entry.text,
28916
- indent: 2
28917
- }));
28918
- case "tool": {
28919
- const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(getPalette().success) }, "⏺");
28920
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(displayText(entry.name)), entry.preview === "" ? "" : ` ${dim(displayText(entry.preview))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
28921
- color: entry.state === "error" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
28922
- wrap: verbose ? "truncate-end" : void 0
28923
- }, ` ⎿ ${displayText(entry.summary)}`), verbose && entry.detail !== void 0 ? (0, import_react.createElement)(ToolDetailBody, { detail: entry.detail }) : void 0);
28924
- }
28925
- case "command": {
28926
- const mark = entry.state === "running" ? (0, import_react.createElement)(Pulse) : entry.state === "error" ? (0, import_react.createElement)(Text, { color: inkColor(getPalette().error) }, "⨯") : (0, import_react.createElement)(Text, { color: inkColor(getPalette().success) }, "⏺");
28927
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, mark, " ", brand(displayText(`/${entry.name}`)), entry.args === "" ? "" : ` ${dim(displayText(entry.args))}`), entry.summary === "" ? void 0 : (0, import_react.createElement)(Text, {
28928
- color: inkColor(getPalette().dim),
28929
- wrap: verbose ? "truncate-end" : void 0
28930
- }, ` ⎿ ${displayText(entry.summary)}`));
28931
- }
28932
- case "turn-marker": return (0, import_react.createElement)(Text, {
28933
- dimColor: true,
28934
- wrap: verbose ? "truncate-end" : void 0
28935
- }, ` ⏹ ${displayText(entry.text)}`);
28936
- case "compaction": return (0, import_react.createElement)(Text, {
28937
- dimColor: true,
28938
- wrap: verbose ? "truncate-end" : void 0
28939
- }, entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${displayText(entry.error)}`);
28940
- case "retry": return (0, import_react.createElement)(Text, {
28941
- color: entry.state === "running" ? inkColor(getPalette().warn) : inkColor(getPalette().dim),
28942
- wrap: verbose ? "truncate-end" : void 0
28943
- }, ` ↻ retry ${entry.attempt}/${entry.max} · ${displayText(entry.code)} · ${Math.round(entry.delayMs / 100) / 10}s`);
28944
- case "files": {
28945
- const shown = entry.paths.slice(0, 3).map((path) => displayText(path)).join(" · ");
28946
- const more = entry.paths.length > 3 ? ` (+${entry.paths.length - 3} more)` : "";
28947
- return (0, import_react.createElement)(Text, {
28948
- dimColor: true,
28949
- wrap: verbose ? "truncate-end" : void 0
28950
- }, ` ⎄ ${shown}${more}`);
28951
- }
28952
- case "pending": return (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, brand("❯ "), displayText(entry.text));
28953
- case "error": return (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, error(displayText(entry.text)));
28954
- default: return assertNever(entry, "transcript entry kind");
28955
- }
29730
+ /** Codex-style panel rhythm that still participates in the row budget. */
29731
+ function PanelGap({ visible }) {
29732
+ return visible ? (0, import_react.createElement)(Text, null, " ") : void 0;
28956
29733
  }
28957
29734
  /**
28958
- * The whale header with a compact three-line copy lockup. The title, bilingual
28959
- * slogan, and key hint stay centered inside the existing eight content rows,
28960
- * preserving the Static header's ten physical rows. Short or narrow terminals
28961
- * keep a one-line form.
29735
+ * The whale header with a compact copy lockup. The dsh kernel version (when
29736
+ * the host manifest resolves), the title, the bilingual slogan, and the key
29737
+ * hint stay centered inside the existing eight content rows, preserving the
29738
+ * Static header's ten physical rows; without a resolvable host the lockup
29739
+ * keeps its historical three lines. Short or narrow terminals keep a one-line
29740
+ * form without the kernel line.
28962
29741
  */
28963
29742
  function Header({ resumed }) {
28964
29743
  const stdout = useStdout().stdout;
28965
29744
  const rows = stdout?.rows ?? 40;
28966
29745
  const columns = stdout?.columns ?? 80;
29746
+ const kernelLine = (() => {
29747
+ const version = dshKernelVersion();
29748
+ return version === void 0 ? void 0 : `dsh-v${version}`;
29749
+ })();
28967
29750
  const title = `DeepSeek Harness · v${DSH_CODE_VERSION}`;
28968
29751
  const slogan = "Into the Unknown 探索未至之境";
28969
29752
  const hint = resumed ? "resumed · /help · Esc interrupt" : "/help · Esc interrupt · Ctrl+C quit";
28970
- const copyColumns = Math.max(visibleColumns(title), visibleColumns(slogan), visibleColumns(hint));
29753
+ const copyWidths = [
29754
+ visibleColumns(title),
29755
+ visibleColumns(slogan),
29756
+ visibleColumns(hint)
29757
+ ];
29758
+ if (kernelLine !== void 0) copyWidths.push(visibleColumns(kernelLine));
29759
+ const copyColumns = Math.max(...copyWidths);
28971
29760
  const compact = `${title} · ${hint}`;
28972
- if (rows < 20 || columns < 26 + copyColumns + 8) return (0, import_react.createElement)(Box, {
29761
+ if (rows < 20 || columns < 26 + copyColumns + 10) return (0, import_react.createElement)(Box, {
28973
29762
  width: Math.max(1, columns - 1),
28974
29763
  borderStyle: "round",
28975
29764
  borderColor: inkColor(getPalette().brand),
@@ -28984,7 +29773,7 @@ function Header({ resumed }) {
28984
29773
  gap: 2,
28985
29774
  borderStyle: "round",
28986
29775
  borderColor: inkColor(getPalette().brand),
28987
- paddingX: 1,
29776
+ paddingX: 2,
28988
29777
  alignSelf: "flex-start"
28989
29778
  }, (0, import_react.createElement)(Box, {
28990
29779
  flexDirection: "column",
@@ -28997,7 +29786,10 @@ function Header({ resumed }) {
28997
29786
  flexDirection: "column",
28998
29787
  width: copyColumns,
28999
29788
  justifyContent: "center"
29000
- }, (0, import_react.createElement)(Text, {
29789
+ }, ...kernelLine === void 0 ? [] : [(0, import_react.createElement)(Text, {
29790
+ color: inkColor(getPalette().dim),
29791
+ wrap: "truncate-end"
29792
+ }, kernelLine)], (0, import_react.createElement)(Text, {
29001
29793
  color: inkColor(getPalette().brandBright),
29002
29794
  bold: true,
29003
29795
  wrap: "truncate-end"
@@ -29041,8 +29833,69 @@ function TodoPanel({ todos }) {
29041
29833
  color: inkColor(getPalette().brand),
29042
29834
  bold: true,
29043
29835
  wrap: "truncate-end"
29044
- }, `todos ${completed}/${todos.length}`, (0, import_react.createElement)(Text, { dimColor: true }, ` · ${inProgress} active · ${pending} pending`), current === void 0 ? "" : (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, ` · ${todoMark(current.status)} ${displayText(current.content)}`)));
29836
+ }, `todos ${completed}/${todos.length}`, (0, import_react.createElement)(Text, { dimColor: true }, ` · ${inProgress} active · ${pending} pending`), current === void 0 ? "" : (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, ` · ${todoMark(current.status)} ${displayText(current.content)}`), (0, import_react.createElement)(Text, { color: inkColor(getPalette().dim) }, " · /todos")));
29837
+ }
29838
+ /**
29839
+ * The /todos subpage: the full todo list in one bounded, scrollable panel.
29840
+ * The live tree's TodoPanel stays a one-row summary; this exclusive view
29841
+ * shows EVERY item with its three-state mark inside the shared panel
29842
+ * viewport (same contract as /help and Ctrl+O: border/title/body/footer all
29843
+ * ride one height budget, the composer and status stay put below).
29844
+ */
29845
+ function TodoListPanel({ todos, onClose }) {
29846
+ const stdout = useStdout().stdout;
29847
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29848
+ const [scroll, setScroll] = (0, import_react.useState)(0);
29849
+ const bodyColumns = Math.max(4, viewport.contentColumns - 4);
29850
+ const completed = todos.filter((todo) => todo.status === "completed").length;
29851
+ const inProgress = todos.filter((todo) => todo.status === "in_progress").length;
29852
+ const pending = todos.length - completed - inProgress;
29853
+ const rows = todos.length === 0 ? [(0, import_react.createElement)(Text, {
29854
+ key: "empty",
29855
+ dimColor: true,
29856
+ wrap: "truncate-end"
29857
+ }, " no todos yet")] : todos.map((todo) => (0, import_react.createElement)(Text, {
29858
+ key: todo.content,
29859
+ dimColor: true,
29860
+ wrap: "truncate-end"
29861
+ }, ` ${todoMark(todo.status)} ${truncateColumns(displayText(todo.content), bodyColumns)}`));
29862
+ const visibleScroll = clampScroll(scroll, rows.length, viewport.bodyRows);
29863
+ const scrollBy = (delta) => {
29864
+ setScroll((current) => moveScroll(current, delta, rows.length, viewport.bodyRows));
29865
+ };
29866
+ (0, import_react.useEffect)(() => {
29867
+ if (visibleScroll !== scroll) setScroll(visibleScroll);
29868
+ }, [visibleScroll, scroll]);
29869
+ useInput((input, key) => {
29870
+ if (key.escape || input === "q") {
29871
+ onClose();
29872
+ return;
29873
+ }
29874
+ if (key.upArrow) scrollBy(-1);
29875
+ else if (key.downArrow) scrollBy(1);
29876
+ else if (key.pageUp) scrollBy(-Math.max(1, viewport.bodyRows - 1));
29877
+ else if (key.pageDown) scrollBy(Math.max(1, viewport.bodyRows - 1));
29878
+ else if (input === "g") setScroll(0);
29879
+ else if (input === "G") setScroll(Math.max(0, rows.length - viewport.bodyRows));
29880
+ });
29881
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
29882
+ if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("todos · esc/q close", viewport.contentColumns));
29883
+ return (0, import_react.createElement)(Box, {
29884
+ flexDirection: "column",
29885
+ width: viewport.outerColumns,
29886
+ paddingX: 1,
29887
+ borderStyle: "round",
29888
+ borderColor: inkColor(getPalette().brand)
29889
+ }, (0, import_react.createElement)(Text, {
29890
+ color: inkColor(getPalette().brand),
29891
+ bold: true,
29892
+ wrap: "truncate-end"
29893
+ }, truncateColumns(`todos · ${completed}/${todos.length} done · ${inProgress} active · ${pending} pending · rows ${rows.length === 0 ? 0 : visibleScroll + 1}-${Math.min(rows.length, visibleScroll + viewport.bodyRows)}/${rows.length}`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), ...rows.slice(visibleScroll, visibleScroll + viewport.bodyRows), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
29894
+ dimColor: true,
29895
+ wrap: "truncate-end"
29896
+ }, dim(truncateColumns("↑↓ scroll · pgup/pgdn page · g/G ends · esc/q close", viewport.contentColumns))));
29045
29897
  }
29898
+ const MemoTodoListPanel = (0, import_react.memo)(TodoListPanel);
29046
29899
  /**
29047
29900
  * Ink props for one status tone: the Codex status-line accent mapping over
29048
29901
  * the DeepSeek palette, all blue by design — the status bar speaks only in
@@ -29125,7 +29978,7 @@ function statusToneProps(tone) {
29125
29978
  * truncation degrades groups, it never wraps a row.
29126
29979
  *
29127
29980
  * The DeepSeek easter egg: when the model label *switches* to an official
29128
- * DeepSeek route, the composer's INPUT ROW (not the frame) plays Codex's
29981
+ * DeepSeek route, the composer's INPUT ROW (the band's middle) plays Codex's
29129
29982
  * effort-ignition "Wave" — a blue crest sweeping the content row column by
29130
29983
  * column, with the `· ✦ ✧` sparkles on the deepseek tier — and the prompt
29131
29984
  * marker keeps the tier accent afterwards. The border stays a constant
@@ -29134,9 +29987,10 @@ function statusToneProps(tone) {
29134
29987
  */
29135
29988
  /** Theme anchors for the one-shot composer wave, read from the active palette
29136
29989
  * so the wave stays coordinated in both themes. The flash tier runs the
29137
- * brand blues; the deepseek tier swaps in the code sky-blue for a brighter,
29138
- * richer mix. Codex's Wave bands carry no hue index (only hues[0] tints the
29139
- * row), so the accent the prompt keeps is always hues[0]. */
29990
+ * brand blues; the deepseek AND unknown tiers swap in the code sky-blue for
29991
+ * a brighter, richer mix (the unknown tier reuses the pro palette). Codex's
29992
+ * Wave bands carry no hue index (only hues[0] tints the row), so the accent
29993
+ * the prompt keeps is always hues[0]. */
29140
29994
  function deepseekWaveHues(tier) {
29141
29995
  const palette = getPalette();
29142
29996
  return tier === "flash" ? [
@@ -29150,10 +30004,28 @@ function deepseekWaveHues(tier) {
29150
30004
  ];
29151
30005
  }
29152
30006
  function StatusLine({ facts, stats, busy, columns, items }) {
29153
- const layout = layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
30007
+ const layout = (0, import_react.useMemo)(() => layoutStatusBar(facts, stats, Math.max(8, columns - 2), {
30008
+ busy,
30009
+ items,
30010
+ contextWidth: Math.max(5, columns - 6)
30011
+ }), [
30012
+ facts.model,
30013
+ facts.mode,
30014
+ facts.cwd,
30015
+ facts.branch,
30016
+ facts.sessionId,
30017
+ facts.title,
30018
+ facts.sandbox,
30019
+ facts.plan,
30020
+ facts.permission,
30021
+ facts.goal?.phase,
30022
+ facts.goal?.rounds,
30023
+ facts.goal?.max,
30024
+ stats,
29154
30025
  busy,
30026
+ columns,
29155
30027
  items
29156
- });
30028
+ ]);
29157
30029
  const renderRow = (row, key, indent = 0) => {
29158
30030
  const leftParts = [];
29159
30031
  row.left.forEach((group, groupIndex) => {
@@ -29522,10 +30394,13 @@ function QuestionBar({ store, snapshot, locked }) {
29522
30394
  returnToOptions();
29523
30395
  return;
29524
30396
  }
29525
- setCustom((current) => current.slice(0, -1));
30397
+ setCustom((current) => deleteLastGrapheme(current));
29526
30398
  return;
29527
30399
  }
29528
- if (input !== "" && !key.ctrl && !key.meta) setCustom((current) => current + input);
30400
+ if (input !== "" && !key.ctrl && !key.meta) {
30401
+ const text = stripPasteMarkers(input);
30402
+ if (text !== "") setCustom((current) => current + text);
30403
+ }
29529
30404
  return;
29530
30405
  }
29531
30406
  if (key.upArrow) {
@@ -29692,7 +30567,7 @@ function providerStateLabel(row) {
29692
30567
  return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
29693
30568
  }
29694
30569
  /** The provider-management stage reached from /model with `a`. */
29695
- function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRetry, onBack }) {
30570
+ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, onRemove, onRetry, onBack }) {
29696
30571
  const stdout = useStdout().stdout;
29697
30572
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29698
30573
  const rows = directory?.rows ?? [];
@@ -29738,6 +30613,11 @@ function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRe
29738
30613
  }
29739
30614
  const target = rows[cursor];
29740
30615
  if (target === void 0) return;
30616
+ if (key.tab) {
30617
+ if (target.settingsNs.length === 0) setActionError("this provider is not managed by Harness settings");
30618
+ else onConfigure(target);
30619
+ return;
30620
+ }
29741
30621
  if (input === "d") {
29742
30622
  const facts = target.credential;
29743
30623
  if (facts?.kind !== "facts" || !facts.configured) setActionError("this provider has no configured API key to remove");
@@ -29809,7 +30689,143 @@ function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRe
29809
30689
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
29810
30690
  color: inkColor(getPalette().dim),
29811
30691
  wrap: "truncate-end"
29812
- }, truncateColumns("↑↓ move · enter add/update key · d remove key · x remove custom provider · r retry · esc back", viewport.contentColumns)));
30692
+ }, truncateColumns("↑↓ move · tab configure · enter add/update key · d remove key · x remove custom provider · r retry · esc back", viewport.contentColumns)));
30693
+ }
30694
+ /** Provider configuration editor: only explicit models are written to settings. */
30695
+ function ProviderConfigurationPanel({ target, catalog, save, done, back }) {
30696
+ const stdout = useStdout().stdout;
30697
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
30698
+ const [baseURL, setBaseURL] = (0, import_react.useState)(target.configuration.baseURL ?? "");
30699
+ const [models, setModels] = (0, import_react.useState)(target.configuration.models);
30700
+ const [cursor, setCursor] = (0, import_react.useState)(0);
30701
+ const [focus, setFocus] = (0, import_react.useState)("url");
30702
+ const [busy, setBusy] = (0, import_react.useState)(false);
30703
+ const [error, setError] = (0, import_react.useState)(void 0);
30704
+ const choices = (0, import_react.useMemo)(() => {
30705
+ const known = catalog.filter((row) => row.provider === target.provider);
30706
+ const ids = new Set(known.map((row) => row.model));
30707
+ return [...known.map((row) => ({
30708
+ id: row.model,
30709
+ name: row.modelName
30710
+ })), ...models.filter((model) => !ids.has(model.id)).map((model) => ({
30711
+ id: model.id,
30712
+ name: model.name ?? model.id
30713
+ }))];
30714
+ }, [
30715
+ catalog,
30716
+ models,
30717
+ target.provider
30718
+ ]);
30719
+ const selected = choices[cursor];
30720
+ const selectedModel = selected === void 0 ? void 0 : models.find((model) => model.id === selected.id);
30721
+ const updateSelected = (change) => {
30722
+ if (selected === void 0) return;
30723
+ setModels((current) => current.some((model) => model.id === selected.id) ? current.map((model) => model.id === selected.id ? {
30724
+ ...model,
30725
+ ...change
30726
+ } : model) : [...current, {
30727
+ id: selected.id,
30728
+ name: selected.name,
30729
+ ...change
30730
+ }]);
30731
+ };
30732
+ const submit = () => {
30733
+ if (busy) return;
30734
+ setBusy(true);
30735
+ setError(void 0);
30736
+ Promise.resolve().then(() => save(target, {
30737
+ ...baseURL.trim() === "" ? {} : { baseURL },
30738
+ models
30739
+ })).then(done, (reason) => {
30740
+ setBusy(false);
30741
+ setError(singleLineText(reason instanceof Error ? reason.message : String(reason)));
30742
+ });
30743
+ };
30744
+ useStableInput((input, key) => {
30745
+ if (busy) return;
30746
+ if (key.escape || input === "q") {
30747
+ back();
30748
+ return;
30749
+ }
30750
+ if (key.tab) {
30751
+ setFocus((current) => current === "url" ? "models" : current === "models" ? "context" : current === "context" ? "output" : "url");
30752
+ return;
30753
+ }
30754
+ if (key.return) {
30755
+ submit();
30756
+ return;
30757
+ }
30758
+ if (focus === "url") {
30759
+ if (key.backspace || key.delete) setBaseURL((current) => deleteLastGrapheme(current));
30760
+ else if (!key.ctrl && !key.meta && input !== "") setBaseURL((current) => current + stripPasteMarkers(input));
30761
+ return;
30762
+ }
30763
+ if (key.upArrow && choices.length > 0) {
30764
+ setCursor((current) => Math.max(0, current - 1));
30765
+ return;
30766
+ }
30767
+ if (key.downArrow && choices.length > 0) {
30768
+ setCursor((current) => Math.min(choices.length - 1, current + 1));
30769
+ return;
30770
+ }
30771
+ if (focus === "models" && input === " ") {
30772
+ if (selectedModel === void 0) updateSelected({});
30773
+ else setModels((current) => current.filter((model) => model.id !== selectedModel.id));
30774
+ return;
30775
+ }
30776
+ if ((focus === "context" || focus === "output") && selectedModel !== void 0) {
30777
+ const field = focus === "context" ? "contextWindow" : "maxTokens";
30778
+ const current = String(selectedModel[field] ?? "");
30779
+ if (key.backspace || key.delete) {
30780
+ const next = current.slice(0, -1);
30781
+ updateSelected({ [field]: next === "" ? void 0 : Number(next) });
30782
+ } else {
30783
+ const digits = stripPasteMarkers(input);
30784
+ if (/^[0-9]+$/u.test(digits)) {
30785
+ const next = `${current}${digits}`;
30786
+ updateSelected({ [field]: Number(next) });
30787
+ }
30788
+ }
30789
+ }
30790
+ }, true);
30791
+ if (viewport.maxHeight === 0) return (0, import_react.createElement)(Box, { display: "none" });
30792
+ const stateRows = error === void 0 ? [] : [(0, import_react.createElement)(Text, {
30793
+ key: "error",
30794
+ color: inkColor(getPalette().error),
30795
+ wrap: "truncate-end"
30796
+ }, truncateColumns(` ${error}`, viewport.contentColumns))];
30797
+ const rowBudget = Math.max(0, viewport.bodyRows - stateRows.length - 1);
30798
+ const first = selectionWindow(cursor, choices.length, rowBudget);
30799
+ const visible = choices.slice(first, first + rowBudget);
30800
+ return (0, import_react.createElement)(Box, {
30801
+ flexDirection: "column",
30802
+ width: viewport.outerColumns,
30803
+ paddingX: 1,
30804
+ borderStyle: "round",
30805
+ borderColor: inkColor(getPalette().brand)
30806
+ }, (0, import_react.createElement)(Text, {
30807
+ color: inkColor(getPalette().brand),
30808
+ bold: true,
30809
+ wrap: "truncate-end"
30810
+ }, truncateColumns(`/model - ${target.displayName} configuration`, viewport.contentColumns)), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
30811
+ color: focus === "url" ? inkColor(getPalette().brandBright) : inkColor(getPalette().dim),
30812
+ wrap: "truncate-end"
30813
+ }, truncateColumns(` ${focus === "url" ? ">" : " "} endpoint: ${baseURL === "" ? "(adapter default)" : baseURL}`, viewport.contentColumns)), ...stateRows, ...visible.map((choice, index) => {
30814
+ const absolute = first + index;
30815
+ const model = models.find((item) => item.id === choice.id);
30816
+ const selectedMark = model === void 0 ? "[ ]" : "[x]";
30817
+ const context = model?.contextWindow === void 0 ? "-" : String(model.contextWindow);
30818
+ const output = model?.maxTokens === void 0 ? "-" : String(model.maxTokens);
30819
+ const active = absolute === cursor && focus !== "url";
30820
+ return (0, import_react.createElement)(Text, {
30821
+ key: choice.id,
30822
+ color: active ? inkColor(getPalette().brandBright) : model === void 0 ? inkColor(getPalette().dim) : inkColor(getPalette().success),
30823
+ wrap: "truncate-end"
30824
+ }, truncateColumns(`${active ? ">" : " "} ${selectedMark} ${choice.name} in:${context} out:${output}`, viewport.contentColumns));
30825
+ }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
30826
+ color: inkColor(getPalette().dim),
30827
+ wrap: "truncate-end"
30828
+ }, truncateColumns("tab endpoint/models/input/output - space select - arrows model - digits set window - enter save - esc back", viewport.contentColumns)));
29813
30829
  }
29814
30830
  /** Write-only masked API-key editor; the secret lives only in this mounted component. */
29815
30831
  function ProviderCredentialPanel({ target, save, done, back }) {
@@ -29852,7 +30868,7 @@ function ProviderCredentialPanel({ target, save, done, back }) {
29852
30868
  return;
29853
30869
  }
29854
30870
  if (key.ctrl || key.meta || input.length === 0) return;
29855
- const next = draft + input;
30871
+ const next = draft + stripPasteMarkers(input);
29856
30872
  if (next.length > 4096) {
29857
30873
  setError("API key input is too long");
29858
30874
  return;
@@ -29991,7 +31007,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
29991
31007
  key: "key-mentions",
29992
31008
  dimColor: true,
29993
31009
  wrap: "truncate-end"
29994
- }, " tab also completes bare workspace paths · @ mentions files and sessions"),
31010
+ }, " @ mentions workspace files and sessions"),
29995
31011
  (0, import_react.createElement)(Text, {
29996
31012
  key: "key-inspector",
29997
31013
  dimColor: true,
@@ -30029,17 +31045,23 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
30029
31045
  (0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
30030
31046
  (0, import_react.createElement)(Box, { key: "local-permission" }, row("/permission", "inspect or select the permission preset (/permission [preset])")),
30031
31047
  (0, import_react.createElement)(Box, { key: "local-new" }, row("/new", "create and switch to a fresh session (/new [preset])")),
31048
+ (0, import_react.createElement)(Box, { key: "local-fork" }, row("/fork", "fork at the latest completed turn (/fork [event-seq])")),
30032
31049
  (0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
30033
31050
  (0, import_react.createElement)(Box, { key: "local-plugin" }, row("/plugin", "inspect the live plugin composition")),
31051
+ (0, import_react.createElement)(Box, { key: "local-jobs" }, row("/jobs", "inspect background jobs")),
30034
31052
  (0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
30035
31053
  (0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
30036
31054
  (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
30037
31055
  (0, import_react.createElement)(Box, { key: "local-agents" }, row("/agents", "inspect subagent sessions of this conversation")),
31056
+ (0, import_react.createElement)(Box, { key: "local-todos" }, row("/todos", "inspect the full todo list")),
30038
31057
  (0, import_react.createElement)(Box, { key: "local-subagent" }, row("/subagent", "choose the model delegated subagents run on")),
30039
31058
  (0, import_react.createElement)(Box, { key: "local-delete" }, row("/delete", "delete a session and its subagent threads")),
30040
31059
  (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
30041
31060
  (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
30042
31061
  (0, import_react.createElement)(Box, { key: "local-title" }, row("/title", "rename this session (/title <text>)")),
31062
+ (0, import_react.createElement)(Box, { key: "local-copy" }, row("/copy", "copy the latest assistant response")),
31063
+ (0, import_react.createElement)(Box, { key: "local-diff" }, row("/diff", "inspect Git changes (/diff [--staged|ref])")),
31064
+ (0, import_react.createElement)(Box, { key: "local-review" }, row("/review", "review Git changes under read-only permissions")),
30043
31065
  (0, import_react.createElement)(Box, { key: "local-quit" }, row("/quit", "exit")),
30044
31066
  ...descriptors.map((descriptor) => (0, import_react.createElement)(Text, {
30045
31067
  key: `command-${descriptor.name}`,
@@ -30102,19 +31124,47 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
30102
31124
  function verboseLine(text, columns) {
30103
31125
  return truncateColumns(displayText(text).replace(/\n/gu, " ↵ ").replace(/\t/gu, " "), Math.max(1, columns));
30104
31126
  }
30105
- /** One-row editor window keeping the logical cursor visible in long drafts. */
31127
+ /** Identify one whole-chunk key sequence Ink drops or blurs. */
31128
+ function annotateRawKey(chunk) {
31129
+ switch (chunk) {
31130
+ case "": return "delete-backward";
31131
+ case "\x1B":
31132
+ case "\x1B\b": return "delete-word-backward";
31133
+ case "\x1B[3~":
31134
+ case "\x1B[3;2~": return "delete-forward";
31135
+ case "\x1B[3;3~":
31136
+ case "\x1B[3;5~": return "delete-word-forward";
31137
+ case "\x1B[H":
31138
+ case "\x1B[1~":
31139
+ case "\x1B[7~":
31140
+ case "\x1BOH": return "home";
31141
+ case "\x1B[F":
31142
+ case "\x1B[4~":
31143
+ case "\x1B[8~":
31144
+ case "\x1BOF": return "end";
31145
+ default: return;
31146
+ }
31147
+ }
31148
+ /**
31149
+ * One-row editor window keeping the logical cursor visible in long drafts.
31150
+ * The caret and its surroundings slice at grapheme boundaries: splitting a
31151
+ * star-plane surrogate pair would render an isolated half under the block
31152
+ * caret with a width the terminal never draws.
31153
+ */
30106
31154
  function editorWindow(value, cursor, columns) {
30107
31155
  const width = Math.max(1, columns);
30108
31156
  const normalize = (text) => displayText(text).replace(/\n/gu, "↵").replace(/\t/gu, " ");
30109
- const caretSource = value.slice(cursor, cursor + 1);
30110
- const caret = caretSource === "" ? " " : normalize(caretSource);
31157
+ const site = clampCursor(value, cursor);
31158
+ const caretSpan = splitGraphemes(value).find((span) => span.start === site);
31159
+ const caret = caretSpan === void 0 ? " " : normalize(caretSpan.text);
31160
+ const rest = value.slice(caretSpan === void 0 ? site : caretSpan.end);
30111
31161
  const remaining = Math.max(0, width - visibleColumns(caret));
30112
- const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(value.slice(cursor + 1))));
31162
+ const afterBudget = Math.min(Math.floor(remaining / 3), visibleColumns(normalize(rest)));
30113
31163
  const beforeBudget = Math.max(0, remaining - afterBudget);
30114
31164
  return {
30115
- before: beforeBudget === 0 ? "" : displayTail(normalize(value.slice(0, cursor)), beforeBudget, 1).text,
31165
+ before: beforeBudget === 0 ? "" : displayTail(normalize(value.slice(0, site)), beforeBudget, 1).text,
30116
31166
  caret,
30117
- after: afterBudget === 0 ? "" : truncateColumns(normalize(value.slice(cursor + 1)), afterBudget)
31167
+ after: afterBudget === 0 ? "" : truncateColumns(normalize(rest), afterBudget)
30118
31168
  };
30119
31169
  }
30120
31170
  /** The empty-composer placeholder text (shared by the static and wave paths). */
@@ -30313,6 +31363,11 @@ function completionCandidates(value, descriptors, skills) {
30313
31363
  description: "start a fresh session",
30314
31364
  origin: "command"
30315
31365
  },
31366
+ {
31367
+ label: "/fork",
31368
+ description: "fork at a completed turn",
31369
+ origin: "command"
31370
+ },
30316
31371
  {
30317
31372
  label: "/resume",
30318
31373
  description: "browse or switch sessions",
@@ -30323,6 +31378,11 @@ function completionCandidates(value, descriptors, skills) {
30323
31378
  description: "inspect the plugin composition",
30324
31379
  origin: "command"
30325
31380
  },
31381
+ {
31382
+ label: "/jobs",
31383
+ description: "inspect background jobs",
31384
+ origin: "command"
31385
+ },
30326
31386
  {
30327
31387
  label: "/statusline",
30328
31388
  description: "customize the status line",
@@ -30343,6 +31403,11 @@ function completionCandidates(value, descriptors, skills) {
30343
31403
  description: "inspect subagent sessions of this conversation",
30344
31404
  origin: "command"
30345
31405
  },
31406
+ {
31407
+ label: "/todos",
31408
+ description: "inspect the full todo list",
31409
+ origin: "command"
31410
+ },
30346
31411
  {
30347
31412
  label: "/subagent",
30348
31413
  description: "choose the model delegated subagents run on",
@@ -30368,6 +31433,21 @@ function completionCandidates(value, descriptors, skills) {
30368
31433
  description: "rename this session",
30369
31434
  origin: "command"
30370
31435
  },
31436
+ {
31437
+ label: "/copy",
31438
+ description: "copy the latest assistant response",
31439
+ origin: "command"
31440
+ },
31441
+ {
31442
+ label: "/diff",
31443
+ description: "inspect Git changes",
31444
+ origin: "command"
31445
+ },
31446
+ {
31447
+ label: "/review",
31448
+ description: "review changes read-only",
31449
+ origin: "command"
31450
+ },
30371
31451
  {
30372
31452
  label: "/quit",
30373
31453
  description: "exit",
@@ -30403,7 +31483,7 @@ function completionCandidates(value, descriptors, skills) {
30403
31483
  }
30404
31484
  /**
30405
31485
  * The completion menu, rendered inside the composer's subtree directly above
30406
- * the framed box — attached the way Claude-Code anchors its dropdown. Opening
31486
+ * the composer band — attached the way Claude-Code anchors its dropdown. Opening
30407
31487
  * it grows the stack downward: the composer stays the last element on screen
30408
31488
  * and everything above (the flushed static transcript, the status line) never
30409
31489
  * moves. Props-only (no lifted state): the menu is a pure view of the input
@@ -30416,7 +31496,7 @@ function CompletionMenu({ active, mention, index, rows }) {
30416
31496
  const terminalRows = stdout?.rows ?? 30;
30417
31497
  if (!active) return void 0;
30418
31498
  const contentColumns = Math.max(1, columns - 4);
30419
- const nameWidth = Math.min(18, Math.max(1, contentColumns - 2), Math.max(0, ...rows.map((row) => visibleColumns(row.label))) + 2);
31499
+ const nameWidth = mention ? Math.max(1, contentColumns - 2) : Math.min(18, Math.max(1, contentColumns - 2), Math.max(0, ...rows.map((row) => visibleColumns(row.label))) + 2);
30420
31500
  const descBudget = Math.max(0, contentColumns - nameWidth - 2);
30421
31501
  const showFooter = terminalRows >= 12;
30422
31502
  const verticalPadding = terminalRows >= 14 ? 1 : 0;
@@ -30454,26 +31534,32 @@ function CompletionMenu({ active, mention, index, rows }) {
30454
31534
  * While a modal (approval / question / model panel) owns the keys, the
30455
31535
  * box passes every key through untouched.
30456
31536
  */
30457
- function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, interrupt, quit, openModel, openEffort, openHelp, openMode, openPermission, openResume, openPlugin, openStatusline, openTheme, openHistory, openAgents, openSubagent, openDelete, deleteConfirm, confirmDelete, cancelDelete, createSession, cancelSessionSwitch, notify, hasNotice, dismissNotice, toggleReasoning, openVerbose, clearView, refresh, loadMentions, cyclePermission, exportTranscript, renameTitle, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle }) {
31537
+ 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, cyclePermission, exportTranscript, renameTitle, copyLastResponse, recallSpace, recordLocal, recordHistory, queued, cancelQueued, historyFill, historyConsumed, waveTier, waveStyle, maxRows, onEditorRows }) {
30458
31538
  const columns = useStdout().stdout?.columns ?? 80;
31539
+ const stdin = useStdin().stdin;
30459
31540
  const [value, setValue] = (0, import_react.useState)("");
30460
31541
  const [cursor, setCursor] = (0, import_react.useState)(0);
30461
- const recall = (0, import_react.useRef)({
30462
- entries: [],
30463
- index: null,
30464
- savedDraft: "",
30465
- lastRecalled: null
30466
- });
31542
+ const killRef = (0, import_react.useRef)("");
31543
+ const preferredColumnRef = (0, import_react.useRef)(null);
31544
+ const editorScrollRef = (0, import_react.useRef)(0);
31545
+ const pasteBracketRef = (0, import_react.useRef)(false);
31546
+ /** Cancels the pending lost-paste safety timer (undefined when disarmed). */
31547
+ const pasteBracketCancelRef = (0, import_react.useRef)(void 0);
31548
+ /** Annotation of the stdin chunk Ink is about to deliver to useInput. */
31549
+ const rawAnnotation = (0, import_react.useRef)(void 0);
31550
+ const recall = (0, import_react.useRef)(beginRecall([], ""));
30467
31551
  (0, import_react.useEffect)(() => {
30468
31552
  if (historyFill === void 0) return;
30469
- setValue(historyFill.text);
30470
- setCursor(historyFill.text.length);
31553
+ const safe = sanitizeDraftText(historyFill.text);
31554
+ setValue(safe);
31555
+ setCursor(safe.length);
31556
+ preferredColumnRef.current = null;
30471
31557
  setDismissedMenuValue(void 0);
30472
31558
  recall.current = {
30473
31559
  entries: recallSpace,
30474
31560
  index: historyFill.index,
30475
- savedDraft: historyFill.text,
30476
- lastRecalled: historyFill.text
31561
+ savedDraft: safe,
31562
+ lastRecalled: safe
30477
31563
  };
30478
31564
  historyConsumed();
30479
31565
  }, [
@@ -30481,6 +31567,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30481
31567
  recallSpace,
30482
31568
  historyConsumed
30483
31569
  ]);
31570
+ (0, import_react.useEffect)(() => {
31571
+ if (stdin === void 0) return;
31572
+ const originalRead = stdin.read.bind(stdin);
31573
+ stdin.read = function patchedRead(...args) {
31574
+ const chunk = originalRead(...args);
31575
+ if (chunk === null) return chunk;
31576
+ const normalized = normalizeKeyboardChunk(typeof chunk === "string" ? chunk : String(chunk));
31577
+ rawAnnotation.current = annotateRawKey(normalized);
31578
+ return normalized;
31579
+ };
31580
+ return () => {
31581
+ stdin.read = originalRead;
31582
+ };
31583
+ }, [stdin]);
30484
31584
  if (recall.current.entries !== recallSpace) {
30485
31585
  const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
30486
31586
  recall.current = {
@@ -30502,27 +31602,6 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30502
31602
  };
30503
31603
  const mentionActive = mentionToken !== void 0;
30504
31604
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
30505
- const bareTokenMatch = /([^\s]+)$/u.exec(lastLine);
30506
- const bareToken = bareTokenMatch === null ? "" : bareTokenMatch[1] ?? "";
30507
- const pathActive = !mentionActive && !bareToken.startsWith("/") && (bareToken.includes("/") || bareToken === "." || bareToken === "..");
30508
- const pathTokenStart = beforeCursor.length - bareToken.length;
30509
- const [pathRows, setPathRows] = (0, import_react.useState)([]);
30510
- (0, import_react.useEffect)(() => {
30511
- if (!active || !pathActive) {
30512
- setPathRows([]);
30513
- return;
30514
- }
30515
- const controller = new AbortController();
30516
- setPathRows([]);
30517
- loadMentions(bareToken, controller.signal).then((rows) => setPathRows(rows.filter((row) => row.kind !== "session")), () => {});
30518
- return () => {
30519
- controller.abort();
30520
- };
30521
- }, [
30522
- active,
30523
- pathActive,
30524
- bareToken
30525
- ]);
30526
31605
  (0, import_react.useEffect)(() => {
30527
31606
  if (!active || !mentionActive) {
30528
31607
  setMentionRows([]);
@@ -30539,15 +31618,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30539
31618
  mentionActive,
30540
31619
  mentionToken?.query
30541
31620
  ]);
30542
- const menuActive = (slashActive || mentionActive || pathActive) && dismissedMenuValue !== value;
31621
+ const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value;
30543
31622
  const menuRows = mentionActive ? mentionRows.map((row) => ({
30544
31623
  label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
30545
31624
  description: row.description,
30546
31625
  origin: "mention"
30547
- })) : pathActive ? pathRows.map((row) => ({
30548
- label: row.label,
30549
- description: row.description,
30550
- origin: "path"
30551
31626
  })) : candidates;
30552
31627
  /** Accept the highlighted completion-menu candidate into the draft. */
30553
31628
  const acceptMenuCandidate = () => {
@@ -30558,13 +31633,6 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30558
31633
  setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
30559
31634
  setCursor(mentionToken.start + insertion.length);
30560
31635
  }
30561
- } else if (pathActive) {
30562
- const row = pathRows[completionIndex % Math.max(1, pathRows.length)];
30563
- if (row !== void 0) {
30564
- const insertion = row.kind === "directory" ? `${row.label}/` : row.label;
30565
- setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor));
30566
- setCursor(pathTokenStart + insertion.length);
30567
- }
30568
31636
  } else {
30569
31637
  const candidate = candidates[completionIndex % candidates.length];
30570
31638
  if (candidate !== void 0) {
@@ -30575,6 +31643,21 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30575
31643
  setCompletionIndex(0);
30576
31644
  setDismissedMenuValue(void 0);
30577
31645
  };
31646
+ /** Apply one editor edit: draft, cursor, kill buffer, menu reset. */
31647
+ const applyEdit = (edit) => {
31648
+ if (edit.killed !== void 0 && edit.killed !== "") killRef.current = edit.killed;
31649
+ setValue(edit.value);
31650
+ setCursor(edit.cursor);
31651
+ preferredColumnRef.current = null;
31652
+ setCompletionIndex(0);
31653
+ setDismissedMenuValue(void 0);
31654
+ };
31655
+ /** Move the cursor without editing; horizontal moves clear the column preference. */
31656
+ const moveCursorTo = (next) => {
31657
+ if (next === cursor) return;
31658
+ setCursor(next);
31659
+ preferredColumnRef.current = null;
31660
+ };
30578
31661
  useInput((input, key) => {
30579
31662
  if (!active) return;
30580
31663
  if (deleteConfirm !== void 0) {
@@ -30610,6 +31693,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30610
31693
  return;
30611
31694
  }
30612
31695
  if (key.ctrl && input === "d") {
31696
+ if (value !== "") {
31697
+ applyEdit(deleteForward(value, cursor));
31698
+ return;
31699
+ }
30613
31700
  if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
30614
31701
  else quit();
30615
31702
  return;
@@ -30630,15 +31717,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30630
31717
  cancelQueued(queued[queued.length - 1].messageId);
30631
31718
  return;
30632
31719
  }
30633
- if (key.return) {
30634
- if (key.meta || key.ctrl && input === "j") {
31720
+ const shiftEnterSequence = input === "[13;2u" || input === "[27;2;13~";
31721
+ if (shiftEnterSequence || key.return) {
31722
+ if (pasteBracketRef.current) {
31723
+ applyEdit(insertText(value, cursor, "\n"));
31724
+ return;
31725
+ }
31726
+ if (shiftEnterSequence || key.shift || key.meta || key.ctrl && input === "j") {
30635
31727
  setValue(value.slice(0, cursor) + "\n" + value.slice(cursor));
30636
31728
  setCursor(cursor + 1);
30637
31729
  setDismissedMenuValue(void 0);
30638
31730
  return;
30639
31731
  }
30640
31732
  if (menuActive) {
30641
- if (!(!mentionActive && !pathActive && candidates.some((candidate) => candidate.label === value))) {
31733
+ if (!(!mentionActive && candidates.some((candidate) => candidate.label === value))) {
30642
31734
  acceptMenuCandidate();
30643
31735
  return;
30644
31736
  }
@@ -30654,12 +31746,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30654
31746
  recordLocal(text);
30655
31747
  recordHistory(text);
30656
31748
  }
30657
- recall.current = {
30658
- entries: recallSpace,
30659
- index: null,
30660
- savedDraft: "",
30661
- lastRecalled: null
30662
- };
31749
+ recall.current = beginRecall(recallSpace, "");
30663
31750
  if (text === "/quit") {
30664
31751
  quit();
30665
31752
  return;
@@ -30683,6 +31770,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30683
31770
  notify(outcome, outcome.startsWith("rename failed:") ? "error" : outcome.startsWith("usage:") || outcome.includes("unavailable") ? "warning" : "info");
30684
31771
  return;
30685
31772
  }
31773
+ if (text === "/copy") {
31774
+ copyLastResponse().then((outcome) => notify(outcome), (error) => notify(`copy failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
31775
+ return;
31776
+ }
31777
+ if (text === "/diff" || text.startsWith("/diff ")) {
31778
+ openDiff(text.slice(5));
31779
+ return;
31780
+ }
31781
+ if (text === "/review" || text.startsWith("/review ")) {
31782
+ reviewChanges(text.slice(7));
31783
+ return;
31784
+ }
30686
31785
  if (text === "/model" || text.startsWith("/model ")) {
30687
31786
  openModel();
30688
31787
  return;
@@ -30717,10 +31816,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30717
31816
  createSession(text.slice(4).trim() || void 0);
30718
31817
  return;
30719
31818
  }
31819
+ if (text === "/fork" || text.startsWith("/fork ")) {
31820
+ forkSession(text.slice(5));
31821
+ return;
31822
+ }
30720
31823
  if (text === "/plugin" || text.startsWith("/plugin ")) {
30721
31824
  openPlugin(text.slice(7).trim());
30722
31825
  return;
30723
31826
  }
31827
+ if (text === "/jobs" || text.startsWith("/jobs ")) {
31828
+ openJobs();
31829
+ return;
31830
+ }
30724
31831
  if (text === "/statusline") {
30725
31832
  openStatusline();
30726
31833
  return;
@@ -30737,6 +31844,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30737
31844
  openAgents();
30738
31845
  return;
30739
31846
  }
31847
+ if (text === "/todos") {
31848
+ openTodos();
31849
+ return;
31850
+ }
30740
31851
  if (text === "/subagent") {
30741
31852
  openSubagent();
30742
31853
  return;
@@ -30760,25 +31871,58 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30760
31871
  setCompletionIndex((index) => (index + 1) % menuRows.length);
30761
31872
  return;
30762
31873
  }
30763
- if (key.upArrow) {
30764
- if (recall.current.entries.length === 0) return;
30765
- const step = recallOlder(recall.current, value);
30766
- recall.current = step.state;
30767
- if (step.entry !== void 0) {
30768
- setValue(step.entry);
30769
- setCursor(step.entry.length);
30770
- setDismissedMenuValue(void 0);
31874
+ const rawKey = rawAnnotation.current;
31875
+ if (rawKey !== void 0) {
31876
+ if (rawKey === "home") moveCursorTo(lineBounds(value, cursor).start);
31877
+ else if (rawKey === "end") moveCursorTo(lineBounds(value, cursor).end);
31878
+ else if (rawKey === "delete-backward") applyEdit(deleteBackward(value, cursor));
31879
+ else if (rawKey === "delete-word-backward") applyEdit(deleteWordBackward(value, cursor));
31880
+ else if (rawKey === "delete-forward") applyEdit(deleteForward(value, cursor));
31881
+ else applyEdit(deleteWordForward(value, cursor));
31882
+ return;
31883
+ }
31884
+ if (key.upArrow || key.downArrow) {
31885
+ if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
31886
+ const step = key.upArrow ? recallOlder(recall.current, value) : recallNewer(recall.current);
31887
+ recall.current = step.state;
31888
+ if (step.entry !== void 0) {
31889
+ const safe = sanitizeDraftText(step.entry);
31890
+ setValue(safe);
31891
+ setCursor(safe.length);
31892
+ preferredColumnRef.current = null;
31893
+ setDismissedMenuValue(void 0);
31894
+ }
31895
+ return;
31896
+ }
31897
+ const model = editorModel(value, Math.max(1, columns - 6));
31898
+ const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
31899
+ const next = moveCursorVertically(model, cursor, preferred, key.upArrow ? -1 : 1);
31900
+ if (next !== cursor) {
31901
+ setCursor(next);
31902
+ preferredColumnRef.current = preferred;
30771
31903
  }
30772
31904
  return;
30773
31905
  }
30774
- if (key.downArrow) {
30775
- if (recall.current.entries.length === 0) return;
30776
- const step = recallNewer(recall.current);
30777
- recall.current = step.state;
30778
- if (step.entry !== void 0) {
30779
- setValue(step.entry);
30780
- setCursor(step.entry.length);
30781
- setDismissedMenuValue(void 0);
31906
+ if (key.ctrl && (input === "p" || input === "n")) {
31907
+ const up = input === "p";
31908
+ if (recall.current.entries.length > 0 && shouldRecallNavigate(value, cursor, recall.current.lastRecalled)) {
31909
+ const step = up ? recallOlder(recall.current, value) : recallNewer(recall.current);
31910
+ recall.current = step.state;
31911
+ if (step.entry !== void 0) {
31912
+ const safe = sanitizeDraftText(step.entry);
31913
+ setValue(safe);
31914
+ setCursor(safe.length);
31915
+ preferredColumnRef.current = null;
31916
+ setDismissedMenuValue(void 0);
31917
+ }
31918
+ return;
31919
+ }
31920
+ const model = editorModel(value, Math.max(1, columns - 6));
31921
+ const preferred = preferredColumnRef.current ?? caretSite(model, cursor).column;
31922
+ const next = moveCursorVertically(model, cursor, preferred, up ? -1 : 1);
31923
+ if (next !== cursor) {
31924
+ setCursor(next);
31925
+ preferredColumnRef.current = preferred;
30782
31926
  }
30783
31927
  return;
30784
31928
  }
@@ -30786,51 +31930,88 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30786
31930
  acceptMenuCandidate();
30787
31931
  return;
30788
31932
  }
30789
- if (key.backspace || key.delete) {
30790
- if (cursor > 0) {
30791
- setValue(value.slice(0, cursor - 1) + value.slice(cursor));
30792
- setCursor(cursor - 1);
30793
- setCompletionIndex(0);
30794
- setDismissedMenuValue(void 0);
30795
- }
30796
- return;
30797
- }
30798
31933
  if (key.leftArrow) {
30799
- setCursor(Math.max(0, cursor - 1));
31934
+ moveCursorTo(key.meta || key.ctrl ? moveWordLeft(value, cursor) : moveCursorBy(value, cursor, -1));
30800
31935
  return;
30801
31936
  }
30802
31937
  if (key.rightArrow) {
30803
- setCursor(Math.min(value.length, cursor + 1));
31938
+ moveCursorTo(key.meta || key.ctrl ? moveWordRight(value, cursor) : moveCursorBy(value, cursor, 1));
30804
31939
  return;
30805
31940
  }
30806
- if (key.ctrl && input === "u") {
30807
- setValue("");
30808
- setCursor(0);
30809
- setDismissedMenuValue(void 0);
31941
+ if (key.meta && input === "b") {
31942
+ moveCursorTo(moveWordLeft(value, cursor));
30810
31943
  return;
30811
31944
  }
30812
- if (key.ctrl && input === "k") {
30813
- setValue(value.slice(0, cursor));
30814
- setDismissedMenuValue(void 0);
31945
+ if (key.meta && input === "f") {
31946
+ moveCursorTo(moveWordRight(value, cursor));
30815
31947
  return;
30816
31948
  }
30817
- if (key.ctrl && input === "l") {
30818
- refresh();
31949
+ if (key.ctrl && input === "b") {
31950
+ moveCursorTo(moveCursorBy(value, cursor, -1));
31951
+ return;
31952
+ }
31953
+ if (key.ctrl && input === "f") {
31954
+ moveCursorTo(moveCursorBy(value, cursor, 1));
31955
+ return;
31956
+ }
31957
+ if (key.ctrl && input === "w") {
31958
+ applyEdit(deleteWordBackward(value, cursor));
31959
+ return;
31960
+ }
31961
+ if (key.meta && input === "d") {
31962
+ applyEdit(deleteWordForward(value, cursor));
31963
+ return;
31964
+ }
31965
+ if (key.backspace || key.delete) {
31966
+ applyEdit(deleteBackward(value, cursor));
30819
31967
  return;
30820
31968
  }
30821
31969
  if (key.ctrl && input === "a") {
30822
- setCursor(0);
31970
+ moveCursorTo(lineBounds(value, cursor).start);
30823
31971
  return;
30824
31972
  }
30825
31973
  if (key.ctrl && input === "e") {
30826
- setCursor(value.length);
31974
+ moveCursorTo(lineBounds(value, cursor).end);
31975
+ return;
31976
+ }
31977
+ if (key.ctrl && input === "u") {
31978
+ applyEdit(killToLineStart(value, cursor));
31979
+ return;
31980
+ }
31981
+ if (key.ctrl && input === "k") {
31982
+ applyEdit(killToLineEnd(value, cursor));
31983
+ return;
31984
+ }
31985
+ if (key.ctrl && input === "y") {
31986
+ if (killRef.current !== "") applyEdit(insertText(value, cursor, killRef.current));
31987
+ return;
31988
+ }
31989
+ if (key.ctrl && input === "l") {
31990
+ refresh();
30827
31991
  return;
30828
31992
  }
30829
31993
  if (input !== "" && !key.ctrl && !key.meta) {
30830
- setValue(value.slice(0, cursor) + input + value.slice(cursor));
30831
- setCursor(cursor + input.length);
30832
- setCompletionIndex(0);
30833
- setDismissedMenuValue(void 0);
31994
+ let text = input;
31995
+ if (text.includes("[200~")) {
31996
+ pasteBracketRef.current = true;
31997
+ pasteBracketCancelRef.current?.();
31998
+ const timer = setTimeout(() => {
31999
+ pasteBracketRef.current = false;
32000
+ pasteBracketCancelRef.current = void 0;
32001
+ }, PASTE_BRACKET_TIMEOUT_MS);
32002
+ pasteBracketCancelRef.current = () => {
32003
+ clearTimeout(timer);
32004
+ pasteBracketCancelRef.current = void 0;
32005
+ };
32006
+ text = text.replaceAll(PASTE_START_MARKER, "");
32007
+ }
32008
+ if (text.includes("[201~")) {
32009
+ pasteBracketRef.current = false;
32010
+ pasteBracketCancelRef.current?.();
32011
+ text = text.replaceAll(PASTE_END_MARKER, "");
32012
+ }
32013
+ if (text === "") return;
32014
+ applyEdit(insertText(value, cursor, text));
30834
32015
  }
30835
32016
  });
30836
32017
  const [waveTick, setWaveTick] = (0, import_react.useState)(null);
@@ -30871,103 +32052,149 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30871
32052
  const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
30872
32053
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
30873
32054
  const promptGlyph = waveTier === "flash" ? "›" : waveTier === "deepseek" ? "»" : "❯";
32055
+ const editorViewModel = editorModel(value, Math.max(1, columns - 6));
32056
+ const clampedCursor = clampCursor(value, cursor);
32057
+ const caret = caretSite(editorViewModel, clampedCursor);
32058
+ const editorWindowRows = Math.min(editorViewModel.rows.length, Math.max(1, maxRows));
32059
+ const maxEditorScroll = Math.max(0, editorViewModel.rows.length - editorWindowRows);
32060
+ const currentEditorScroll = Math.min(Math.max(0, editorScrollRef.current), maxEditorScroll);
32061
+ const editorWindowStart = caret.row < currentEditorScroll ? caret.row : caret.row >= currentEditorScroll + editorWindowRows ? caret.row - editorWindowRows + 1 : currentEditorScroll;
32062
+ editorScrollRef.current = editorWindowStart;
32063
+ const editorRowCount = frozen ? 1 : editorWindowRows;
32064
+ (0, import_react.useEffect)(() => {
32065
+ onEditorRows(editorRowCount);
32066
+ }, [editorRowCount, onEditorRows]);
32067
+ const bandWidth = Math.max(1, columns - 1);
32068
+ const bandBg = inkColor(getPalette().composerBand);
32069
+ const bandFill = (consumed) => " ".repeat(Math.max(0, bandWidth - consumed));
32070
+ const band = (content) => (0, import_react.createElement)(Box, {
32071
+ flexDirection: "column",
32072
+ width: bandWidth
32073
+ }, (0, import_react.createElement)(Text, { backgroundColor: bandBg }, " ".repeat(bandWidth)), content, (0, import_react.createElement)(Text, { backgroundColor: bandBg }, " ".repeat(bandWidth)));
30874
32074
  if (frozen) {
30875
- if (deleteConfirm !== void 0) return (0, import_react.createElement)(Box, {
30876
- width: Math.max(1, columns - 1),
30877
- borderStyle: "round",
30878
- borderColor: inkColor(getPalette().warn),
30879
- paddingX: 1
30880
- }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, {
30881
- color: inkColor(getPalette().warn),
30882
- bold: true
30883
- }, "❯ "), (0, import_react.createElement)(Text, {
30884
- color: inkColor(getPalette().warn),
30885
- bold: true
30886
- }, "y delete · any other key cancels")));
30887
- const frozen = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
30888
- return (0, import_react.createElement)(Box, {
30889
- width: Math.max(1, columns - 1),
30890
- borderStyle: "round",
30891
- borderColor: inkColor(getPalette().dim),
30892
- paddingX: 1
30893
- }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, {
32075
+ if (deleteConfirm !== void 0) {
32076
+ const warning = "y delete · any other key cancels";
32077
+ return band((0, import_react.createElement)(Text, {
32078
+ backgroundColor: bandBg,
32079
+ wrap: "truncate-end"
32080
+ }, (0, import_react.createElement)(Text, {
32081
+ color: inkColor(getPalette().warn),
32082
+ bold: true
32083
+ }, "❯ "), (0, import_react.createElement)(Text, {
32084
+ color: inkColor(getPalette().warn),
32085
+ bold: true
32086
+ }, warning), bandFill(2 + visibleColumns(warning))));
32087
+ }
32088
+ const frozenLine = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
32089
+ return band((0, import_react.createElement)(Text, {
32090
+ backgroundColor: bandBg,
32091
+ wrap: "truncate-end"
32092
+ }, (0, import_react.createElement)(Text, {
30894
32093
  color: promptColor,
30895
32094
  bold: tierActive ? true : void 0
30896
- }, busy ? "… " : `${promptGlyph} `), frozen));
32095
+ }, busy ? "… " : `${promptGlyph} `), frozenLine, bandFill(2 + visibleColumns(frozenLine))));
30897
32096
  }
30898
- const frame = (row, borderRgb) => (0, import_react.createElement)(Box, {
30899
- width: Math.max(1, columns - 1),
30900
- borderStyle: "round",
30901
- borderColor: inkColor(borderRgb ?? getPalette().dim),
30902
- paddingX: 1
30903
- }, row);
30904
32097
  const menu = (0, import_react.createElement)(CompletionMenu, {
30905
32098
  active: menuActive,
30906
32099
  mention: mentionActive,
30907
32100
  index: completionIndex,
30908
32101
  rows: menuRows
30909
32102
  });
30910
- const editor = editorWindow(value, cursor, Math.max(1, columns - 6));
30911
- const staticRow = (0, import_react.createElement)(Text, { wrap: "truncate-end" }, busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
30912
- color: promptColor,
30913
- bold: tierActive ? true : void 0
30914
- }, `${promptGlyph} `), value === "" ? void 0 : editor.before, (0, import_react.createElement)(CursorBlock, { char: editor.caret }), value === "" && !busy ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : editor.after);
32103
+ const editorRows = [];
32104
+ for (let index = editorWindowStart; index < Math.min(editorViewModel.rows.length, editorWindowStart + editorWindowRows); index += 1) {
32105
+ const row = editorViewModel.rows[index];
32106
+ const caretAt = index === caret.row ? row.offsets.indexOf(clampedCursor) : -1;
32107
+ const before = caretAt > 0 ? row.text.slice(0, row.cuts[caretAt]) : "";
32108
+ const caretChar = caretAt >= 0 && caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt], row.cuts[caretAt + 1]) : " ";
32109
+ const after = caretAt < 0 ? row.text : caretAt < row.cuts.length - 1 ? row.text.slice(row.cuts[caretAt + 1]) : "";
32110
+ const placeholder = index === 0 && value === "" && !busy;
32111
+ const tail = placeholder ? COMPOSER_PLACEHOLDER : after;
32112
+ const consumed = 2 + visibleColumns(before) + visibleColumns(caretChar) + visibleColumns(tail);
32113
+ editorRows.push((0, import_react.createElement)(Text, {
32114
+ key: index,
32115
+ backgroundColor: bandBg,
32116
+ wrap: "truncate-end"
32117
+ }, index === 0 ? busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
32118
+ color: promptColor,
32119
+ bold: tierActive ? true : void 0
32120
+ }, `${promptGlyph} `) : " ", before, (0, import_react.createElement)(CursorBlock, {
32121
+ key: "caret",
32122
+ char: caretChar
32123
+ }), placeholder ? (0, import_react.createElement)(Text, { dimColor: true }, COMPOSER_PLACEHOLDER) : after, bandFill(consumed)));
32124
+ }
32125
+ const staticEditor = (0, import_react.createElement)(Box, { flexDirection: "column" }, ...editorRows);
30915
32126
  const waveRow = () => {
30916
- const contentWidth = Math.max(1, columns - 5);
30917
- const waveEditor = editorWindow(value, cursor, Math.max(1, contentWidth - 2));
30918
32127
  const hues = deepseekWaveHues(waveTier);
30919
32128
  const style = waveStyle;
30920
- const base = getTheme() === "light" ? WAVE_BASE_LIGHT : WAVE_BASE_DARK;
30921
- const waveBg = (column) => {
30922
- const rgb = deepseekWaveColumnBg(waveTick, column, contentWidth, waveTier, style, hues, base);
30923
- return rgb === null ? void 0 : inkColor(rgb);
32129
+ const bandRgb = getPalette().composerBand;
32130
+ const waveBg = (row, column) => {
32131
+ const rgb = deepseekWaveColumnBg(waveTick, column, bandWidth, waveTier, style, hues, bandRgb, row, 3);
32132
+ return rgb === null ? bandBg : inkColor(rgb);
30924
32133
  };
30925
- const cells = [];
30926
- cells.push({
30927
- char: promptGlyph,
30928
- color: promptColor,
30929
- bold: true,
30930
- backgroundColor: waveBg(0)
30931
- });
30932
- cells.push({
30933
- char: " ",
30934
- color: promptColor,
30935
- backgroundColor: waveBg(1)
30936
- });
32134
+ const blankBandRow = (row) => {
32135
+ const blanks = [];
32136
+ while (blanks.length < bandWidth) blanks.push({
32137
+ char: " ",
32138
+ backgroundColor: waveBg(row, blanks.length)
32139
+ });
32140
+ return (0, import_react.createElement)(Text, { key: row }, ...waveRowSpans(blanks));
32141
+ };
32142
+ const waveEditor = editorWindow(value, cursor, Math.max(1, columns - 7));
32143
+ const cells = [
32144
+ {
32145
+ char: " ",
32146
+ backgroundColor: waveBg(1, 0)
32147
+ },
32148
+ {
32149
+ char: " ",
32150
+ backgroundColor: waveBg(1, 1)
32151
+ },
32152
+ {
32153
+ char: promptGlyph,
32154
+ color: promptColor,
32155
+ bold: true,
32156
+ backgroundColor: waveBg(1, 2)
32157
+ },
32158
+ {
32159
+ char: " ",
32160
+ color: promptColor,
32161
+ backgroundColor: waveBg(1, 3)
32162
+ }
32163
+ ];
30937
32164
  for (const char of waveEditor.before) cells.push({
30938
32165
  char,
30939
- backgroundColor: waveBg(cells.length)
32166
+ backgroundColor: waveBg(1, cells.length)
30940
32167
  });
30941
32168
  cells.push({
30942
32169
  char: waveEditor.caret,
30943
32170
  inverse: true,
30944
- backgroundColor: waveBg(cells.length)
32171
+ backgroundColor: waveBg(1, cells.length)
30945
32172
  });
30946
32173
  if (value === "" && !busy) for (let at = 0; at < 40; at += 1) cells.push({
30947
32174
  char: COMPOSER_PLACEHOLDER[at],
30948
32175
  dim: true,
30949
- backgroundColor: waveBg(cells.length)
32176
+ backgroundColor: waveBg(1, cells.length)
30950
32177
  });
30951
32178
  else for (const char of waveEditor.after) cells.push({
30952
32179
  char,
30953
- backgroundColor: waveBg(cells.length)
32180
+ backgroundColor: waveBg(1, cells.length)
30954
32181
  });
30955
- while (cells.length < contentWidth) cells.push({
32182
+ while (cells.length < bandWidth) cells.push({
30956
32183
  char: " ",
30957
- backgroundColor: waveBg(cells.length)
32184
+ backgroundColor: waveBg(1, cells.length)
30958
32185
  });
30959
32186
  if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
30960
- const word = "deepseek";
30961
- const start = Math.max(2, Math.floor((contentWidth - 8) / 2));
32187
+ const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
32188
+ const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
30962
32189
  let clear = true;
30963
- for (let at = 0; at < 8; at += 1) {
32190
+ for (let at = 0; at < word.length; at += 1) {
30964
32191
  const cell = cells[start + at];
30965
32192
  if (cell === void 0 || cell.char !== " " && cell.dim !== true) {
30966
32193
  clear = false;
30967
32194
  break;
30968
32195
  }
30969
32196
  }
30970
- if (clear) for (let at = 0; at < 8; at += 1) {
32197
+ if (clear) for (let at = 0; at < word.length; at += 1) {
30971
32198
  const cell = cells[start + at];
30972
32199
  cell.char = word[at];
30973
32200
  cell.color = inkColor(deepseekWaveWordHue(at, hues));
@@ -30975,7 +32202,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30975
32202
  cell.dim = false;
30976
32203
  }
30977
32204
  }
30978
- if (waveTier === "deepseek" && style === "wave") {
32205
+ if ((waveTier === "deepseek" || waveTier === "unknown") && style === "wave") {
30979
32206
  const spark = deepseekWaveSpark(waveTick);
30980
32207
  if (spark !== null) {
30981
32208
  const last = cells[cells.length - 1];
@@ -30987,24 +32214,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30987
32214
  }
30988
32215
  }
30989
32216
  }
30990
- const borderRgb = deepseekWaveBorderColor(waveTick, waveTier, style, hues, getPalette().dim);
30991
- return frame((0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...waveRowSpans(cells)), borderRgb);
32217
+ return (0, import_react.createElement)(Box, {
32218
+ flexDirection: "column",
32219
+ width: bandWidth
32220
+ }, blankBandRow(0), (0, import_react.createElement)(Text, { wrap: "truncate-end" }, ...waveRowSpans(cells)), blankBandRow(2));
30992
32221
  };
30993
- return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : frame(staticRow));
32222
+ return (0, import_react.createElement)(Box, { flexDirection: "column" }, menu, waveTick !== null && waveTier !== null && !busy ? waveRow() : band(staticEditor));
30994
32223
  }
30995
- /** Build one settled row (row Box plus its roomy-prompt spacers). */
30996
- function buildSettledRow(entry, index, showReasoning) {
30997
- const row = (0, import_react.createElement)(EntryLine, {
30998
- entry,
30999
- showReasoning,
31000
- verbose: false
31001
- });
32224
+ /** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
32225
+ function buildSettledRow(entry, index, showReasoning, columns) {
31002
32226
  const roomyPrompt = entry.kind === "user" && !entry.notice;
32227
+ const lines = settledEntryLines(entry, Math.max(10, columns - 2), showReasoning);
31003
32228
  return {
31004
- box: (0, import_react.createElement)(Box, {
31005
- key: index,
31006
- paddingX: 1
31007
- }, row),
32229
+ box: (0, import_react.createElement)(Box, { key: index }, (0, import_react.createElement)(StyledRows, { lines })),
31008
32230
  before: roomyPrompt ? (0, import_react.createElement)(Box, {
31009
32231
  key: `prompt-before-${index}`,
31010
32232
  paddingX: 1
@@ -31013,10 +32235,17 @@ function buildSettledRow(entry, index, showReasoning) {
31013
32235
  key: `prompt-after-${index}`,
31014
32236
  paddingX: 1
31015
32237
  }, (0, import_react.createElement)(Text, null, " ")) : void 0,
31016
- reasonSensitive: entry.kind === "assistant" && entry.reasoning !== "",
31017
- showReasoning
32238
+ rows: lines.length + (roomyPrompt ? 2 : 0)
31018
32239
  };
31019
32240
  }
32241
+ /** The dim hint row placed under the header once the window has dropped entries. */
32242
+ function settledTrimHint(droppedEntries, columns) {
32243
+ return (0, import_react.createElement)(Text, {
32244
+ key: "history-cap-hint",
32245
+ color: inkColor(getPalette().dim),
32246
+ wrap: "truncate-end"
32247
+ }, truncateColumns(`… +${droppedEntries} earlier messages hidden · ctrl+o browse · /export full transcript`, Math.max(10, columns - 2)));
32248
+ }
31020
32249
  /**
31021
32250
  * The settled `<Static>` row set as a PURE incremental state machine (App
31022
32251
  * drives it from the memo; tests drive it directly and read `built`).
@@ -31030,77 +32259,82 @@ function buildSettledRow(entry, index, showReasoning) {
31030
32259
  * rebuild of rows, Map, or MarkdownBody parses). `records` is mutated in place
31031
32260
  * on the append/toggle paths to stay O(delta).
31032
32261
  *
32262
+ * RENDERED-HISTORY CAP: the window holds at most `rowCap` physical rows of
32263
+ * settled transcript (header and hint reserved on top). The cap exists only
32264
+ * here — the event log, the store projection, /export, Ctrl+O, and /resume
32265
+ * keep the full history. Ink 5's <Static> is a consumption counter
32266
+ * (items.slice(index) keyed on length): deleting head items mid-stream while
32267
+ * appending tail items can permanently swallow new rows, so the append branch
32268
+ * NEVER drops the head — it only accounts rows and flags `needsTrim` once the
32269
+ * window overflows cap + margin. The flag fires one source-backed replay
32270
+ * (epoch bump = the existing clear + <Static> remount), whose rebuild branch
32271
+ * walks the settled entries BACKWARD from the newest, keeps whole entries
32272
+ * until the cap, and counts everything older as `droppedEntries` (those
32273
+ * entries never even reach settledEntryLines). Hysteresis bounds replays to
32274
+ * at most one per 25% growth; resize / Ctrl+L / idle Ctrl+R replays re-window
32275
+ * for free on the same path.
32276
+ *
31033
32277
  * Full rebuilds run only on the rare, deliberate paths: no cache yet, a
31034
- * source-backed replay (`epoch` bump: resize / Ctrl+L / Ctrl+R remounts
31035
- * `<Static>` and must re-flush the CURRENT rows), a `resumed` change, or a shrink
31036
- * (`store.reset`). A reasoning toggle rebuilds only the rows whose text
31037
- * depends on it, preserving the other rows' element identity.
32278
+ * source-backed replay (`epoch` bump: resize / Ctrl+L / an idle Ctrl+R fold
32279
+ * toggle / a cap trim remounts `<Static>` and must re-flush the CURRENT rows
32280
+ * at the CURRENT fold state), a `resumed` change, or a shrink (`store.reset`).
32281
+ * While a turn is busy or streaming, Ctrl+R only flips the live region; rows
32282
+ * already emitted to native scrollback change exclusively through rebuilds.
31038
32283
  */
31039
- function computeSettledRows(previous, entries, settled, showReasoning, resumed, epoch) {
32284
+ function computeSettledRows(previous, entries, settled, showReasoning, resumed, epoch, columns = 80, rowCap = SETTLED_ROW_CAP) {
31040
32285
  if (previous === void 0 || previous.epoch !== epoch || previous.resumed !== resumed || settled < previous.entries.length) {
31041
32286
  const records = /* @__PURE__ */ new Map();
31042
- const flat = [(0, import_react.createElement)(Header, {
31043
- key: "header",
31044
- resumed
31045
- })];
31046
- for (let index = 0; index < settled; index++) {
32287
+ const window = [];
32288
+ let windowRows = 0;
32289
+ let droppedEntries = 0;
32290
+ let index = settled - 1;
32291
+ for (; index >= 0; index--) {
31047
32292
  const entry = entries[index];
31048
- const record = buildSettledRow(entry, index, showReasoning);
32293
+ if (entry === void 0) break;
32294
+ const record = buildSettledRow(entry, index, showReasoning, columns);
32295
+ if (rowCap > 0 && windowRows + record.rows > rowCap - SETTLED_ROW_RESERVE) {
32296
+ droppedEntries = index + 1;
32297
+ break;
32298
+ }
31049
32299
  records.set(entry, record);
31050
- if (record.before !== void 0) flat.push(record.before);
31051
- flat.push(record.box);
31052
- if (record.after !== void 0) flat.push(record.after);
32300
+ windowRows += record.rows;
32301
+ if (record.after !== void 0) window.unshift(record.after);
32302
+ window.unshift(record.box);
32303
+ if (record.before !== void 0) window.unshift(record.before);
31053
32304
  }
32305
+ const header = (0, import_react.createElement)(Header, {
32306
+ key: "header",
32307
+ resumed
32308
+ });
32309
+ const flat = droppedEntries > 0 ? [
32310
+ header,
32311
+ settledTrimHint(droppedEntries, columns),
32312
+ ...window
32313
+ ] : [header, ...window];
31054
32314
  return {
31055
32315
  cache: {
31056
- entries: entries.slice(0, settled),
32316
+ entries: entries.slice(droppedEntries, settled),
31057
32317
  records,
31058
- header: flat[0],
32318
+ header,
31059
32319
  resumed,
31060
32320
  showReasoning,
31061
32321
  epoch,
31062
- flat
31063
- },
31064
- built: settled
31065
- };
31066
- }
31067
- if (previous.showReasoning !== showReasoning) {
31068
- const records = previous.records;
31069
- const flat = [previous.header];
31070
- let built = 0;
31071
- for (let index = 0; index < previous.entries.length; index++) {
31072
- const entry = previous.entries[index];
31073
- const record = records.get(entry);
31074
- const current = record.reasonSensitive ? {
31075
- ...record,
31076
- box: (0, import_react.createElement)(Box, {
31077
- key: index,
31078
- paddingX: 1
31079
- }, (0, import_react.createElement)(EntryLine, {
31080
- entry,
31081
- showReasoning,
31082
- verbose: false
31083
- })),
31084
- showReasoning
31085
- } : record;
31086
- if (current !== record) {
31087
- records.set(entry, current);
31088
- built += 1;
31089
- }
31090
- if (current.before !== void 0) flat.push(current.before);
31091
- flat.push(current.box);
31092
- if (current.after !== void 0) flat.push(current.after);
31093
- }
31094
- return {
31095
- cache: {
31096
- ...previous,
31097
- records,
31098
- showReasoning,
31099
- flat
32322
+ columns,
32323
+ flat,
32324
+ droppedEntries,
32325
+ totalRows: windowRows,
32326
+ needsTrim: false
31100
32327
  },
31101
- built
32328
+ built: records.size
31102
32329
  };
31103
32330
  }
32331
+ if (previous.showReasoning !== showReasoning) return {
32332
+ cache: {
32333
+ ...previous,
32334
+ showReasoning
32335
+ },
32336
+ built: 0
32337
+ };
31104
32338
  if (settled === previous.entries.length) return {
31105
32339
  cache: previous,
31106
32340
  built: 0
@@ -31108,15 +32342,19 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
31108
32342
  const records = previous.records;
31109
32343
  const suffix = [];
31110
32344
  const added = [];
31111
- for (let index = previous.entries.length; index < settled; index++) {
32345
+ let deltaRows = 0;
32346
+ for (let index = previous.entries.length + previous.droppedEntries; index < settled; index++) {
31112
32347
  const entry = entries[index];
31113
- const record = buildSettledRow(entry, index, showReasoning);
32348
+ const record = buildSettledRow(entry, index, showReasoning, previous.columns);
31114
32349
  records.set(entry, record);
31115
32350
  suffix.push(entry);
32351
+ deltaRows += record.rows;
31116
32352
  if (record.before !== void 0) added.push(record.before);
31117
32353
  added.push(record.box);
31118
32354
  if (record.after !== void 0) added.push(record.after);
31119
32355
  }
32356
+ const totalRows = previous.totalRows + deltaRows;
32357
+ const needsTrim = rowCap > 0 && totalRows > rowCap + Math.floor(rowCap / 4);
31120
32358
  return {
31121
32359
  cache: {
31122
32360
  entries: previous.entries.concat(suffix),
@@ -31125,9 +32363,13 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
31125
32363
  resumed: previous.resumed,
31126
32364
  showReasoning,
31127
32365
  epoch: previous.epoch,
31128
- flat: previous.flat.concat(added)
32366
+ columns: previous.columns,
32367
+ flat: previous.flat.concat(added),
32368
+ droppedEntries: previous.droppedEntries,
32369
+ totalRows,
32370
+ needsTrim
31129
32371
  },
31130
- built: settled - previous.entries.length
32372
+ built: suffix.length
31131
32373
  };
31132
32374
  }
31133
32375
  /** The whole terminal app; state arrives via the store, output via Ink. */
@@ -31146,16 +32388,18 @@ function App(props) {
31146
32388
  const [effortFor, setEffortFor] = (0, import_react.useState)(void 0);
31147
32389
  /** Effective reasoning effort, shown in the /model picker and switch notice. */
31148
32390
  const [effortLabel, setEffortLabel] = (0, import_react.useState)(props.effort);
31149
- /** DeepSeek easter egg: switching INTO an official DeepSeek route plays
31150
- * one of Codex's three ignition styles (Wave / Aurora / Pulse, picked at
31151
- * random without repeating) across the composer's padded band (33ms tick,
31152
- * per-style durations), then the band returns to static while the prompt
31153
- * marker keeps the tier accent. The trigger follows the applied model
31154
- * label (what the status bar actually shows), never the initial paint,
31155
- * and the tier is derived from the label and cached at the switch. The
31156
- * 33ms tick itself lives inside Input, so the sweep re-renders only the
31157
- * composer row, not the whole tree, at 30fps; App owns the rarely-changing
31158
- * tier/style and Input starts the sweep whenever that pair changes. */
32391
+ /** DeepSeek easter egg: switching INTO an official DeepSeek route — or
32392
+ * onto a NON-DeepSeek model running a reasoning effort strictly above
32393
+ * high plays one of Codex's three ignition styles (Wave / Aurora /
32394
+ * Pulse, picked at random without repeating) across the composer's
32395
+ * padded band (33ms tick, per-style durations), then the band returns
32396
+ * to static while the prompt marker keeps the tier accent. The trigger
32397
+ * follows the applied model label (what the status bar actually shows),
32398
+ * never the initial paint, and the tier is derived from the label and
32399
+ * cached at the switch. The 33ms tick itself lives inside Input, so the
32400
+ * sweep re-renders only the composer band, not the whole tree, at 30fps;
32401
+ * App owns the rarely-changing tier/style and Input starts the sweep
32402
+ * whenever that pair changes. */
31159
32403
  const [waveTier, setWaveTier] = (0, import_react.useState)(null);
31160
32404
  const [waveStyle, setWaveStyle] = (0, import_react.useState)(null);
31161
32405
  const previousModel = (0, import_react.useRef)(void 0);
@@ -31167,13 +32411,15 @@ function App(props) {
31167
32411
  const effortChanged = previousEffort.current !== effortLabel;
31168
32412
  previousEffort.current = effortLabel;
31169
32413
  const modelChanged = previous !== void 0 && previous !== modelLabel;
31170
- if (!isOfficialDeepSeekLabel(modelLabel)) {
32414
+ const official = isOfficialDeepSeekLabel(modelLabel);
32415
+ const unknownTrigger = !official && effortAboveHigh(effortLabel);
32416
+ if (!official && !unknownTrigger) {
31171
32417
  setWaveTier(null);
31172
32418
  setWaveStyle(null);
31173
32419
  return;
31174
32420
  }
31175
32421
  if (modelChanged || effortChanged) {
31176
- setWaveTier(deepseekWaveTier(modelLabel));
32422
+ setWaveTier(official ? deepseekWaveTier(modelLabel) : "unknown");
31177
32423
  const nextStyle = deepseekWaveStyleRandom(previousStyle.current);
31178
32424
  previousStyle.current = nextStyle;
31179
32425
  setWaveStyle(nextStyle);
@@ -31242,18 +32488,21 @@ function App(props) {
31242
32488
  const busy = view.busy;
31243
32489
  const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
31244
32490
  const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
32491
+ const [diffView, setDiffView] = (0, import_react.useState)(void 0);
31245
32492
  const [helpOpen, setHelpOpen] = (0, import_react.useState)(false);
31246
32493
  const [modeOpen, setModeOpen] = (0, import_react.useState)(false);
31247
32494
  const [permissionOpen, setPermissionOpen] = (0, import_react.useState)(false);
31248
32495
  const [resumeOpen, setResumeOpen] = (0, import_react.useState)(false);
31249
32496
  const [pluginOpen, setPluginOpen] = (0, import_react.useState)(false);
31250
32497
  const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
32498
+ const [jobsOpen, setJobsOpen] = (0, import_react.useState)(false);
31251
32499
  const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
31252
32500
  const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
31253
32501
  const [themeOpen, setThemeOpen] = (0, import_react.useState)(false);
31254
32502
  const [historyOpen, setHistoryOpen] = (0, import_react.useState)(false);
31255
32503
  const [agentsOpen, setAgentsOpen] = (0, import_react.useState)(false);
31256
32504
  const [subagentOpen, setSubagentOpen] = (0, import_react.useState)(false);
32505
+ const [todosOpen, setTodosOpen] = (0, import_react.useState)(false);
31257
32506
  /** /delete state: delete-mode hint plus an optional pre-armed row id. */
31258
32507
  const [resumeDelete, setResumeDelete] = (0, import_react.useState)({ mode: false });
31259
32508
  /** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
@@ -31318,7 +32567,7 @@ function App(props) {
31318
32567
  const agentRows = (0, import_react.useSyncExternalStore)(props.subagents.subscribe, props.subagents.getSnapshot);
31319
32568
  const approvalPending = approvalSnapshot.pending !== void 0;
31320
32569
  const questionPending = questionSnapshot.pending !== void 0;
31321
- const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
32570
+ const inputActive = deleteConfirmId !== void 0 ? !approvalPending && !questionPending : !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
31322
32571
  (0, import_react.useEffect)(() => {
31323
32572
  if (!approvalPending && !questionPending) return;
31324
32573
  setModelOpen(false);
@@ -31335,12 +32584,20 @@ function App(props) {
31335
32584
  setHistoryOpen(false);
31336
32585
  setAgentsOpen(false);
31337
32586
  setSubagentOpen(false);
32587
+ setTodosOpen(false);
31338
32588
  setDeleteConfirmId(void 0);
31339
32589
  setVerboseOpen(false);
32590
+ setDiffView(void 0);
31340
32591
  }, [approvalPending, questionPending]);
32592
+ const appStdout = useStdout().stdout;
32593
+ const [terminalSize, setTerminalSize] = (0, import_react.useState)(() => ({
32594
+ columns: appStdout?.columns ?? 80,
32595
+ rows: appStdout?.rows ?? 30
32596
+ }));
32597
+ const terminalSizeRef = (0, import_react.useRef)(terminalSize);
31341
32598
  const settledRowsCache = (0, import_react.useRef)(void 0);
31342
32599
  const settledRows = (0, import_react.useMemo)(() => {
31343
- const result = computeSettledRows(settledRowsCache.current, view.entries, settled, showReasoning, props.resumed, refreshEpoch);
32600
+ const result = computeSettledRows(settledRowsCache.current, view.entries, settled, showReasoning, props.resumed, refreshEpoch, terminalSize.columns);
31344
32601
  settledRowsCache.current = result.cache;
31345
32602
  return result.cache.flat;
31346
32603
  }, [
@@ -31348,14 +32605,10 @@ function App(props) {
31348
32605
  settled,
31349
32606
  showReasoning,
31350
32607
  props.resumed,
31351
- refreshEpoch
32608
+ refreshEpoch,
32609
+ terminalSize.columns
31352
32610
  ]);
31353
- const appStdout = useStdout().stdout;
31354
- const [terminalSize, setTerminalSize] = (0, import_react.useState)(() => ({
31355
- columns: appStdout?.columns ?? 80,
31356
- rows: appStdout?.rows ?? 30
31357
- }));
31358
- const terminalSizeRef = (0, import_react.useRef)(terminalSize);
32611
+ const synchronizedReplayPending = (0, import_react.useRef)(false);
31359
32612
  (0, import_react.useEffect)(() => {
31360
32613
  if (appStdout === void 0) return;
31361
32614
  let replayTimer;
@@ -31369,7 +32622,8 @@ function App(props) {
31369
32622
  setTerminalSize(next);
31370
32623
  if (replayTimer !== void 0) clearTimeout(replayTimer);
31371
32624
  replayTimer = setTimeout(() => {
31372
- appStdout.write(RESIZE_REFLOW_CLEAR);
32625
+ synchronizedReplayPending.current = true;
32626
+ appStdout.write("\x1B[?2026h\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
31373
32627
  setRefreshEpoch((epoch) => epoch + 1);
31374
32628
  }, RESIZE_REFLOW_DELAY_MS);
31375
32629
  };
@@ -31382,28 +32636,51 @@ function App(props) {
31382
32636
  const terminalRows = terminalSize.rows;
31383
32637
  const terminalColumns = terminalSize.columns;
31384
32638
  const composerGutterRows = layoutGutterRows(terminalRows);
31385
- const dynamicRows = Math.max(1, terminalRows - 13 - composerGutterRows);
32639
+ const [composerRows, setComposerRows] = (0, import_react.useState)(1);
32640
+ const handleEditorRows = (0, import_react.useCallback)((rows) => {
32641
+ setComposerRows((current) => current === rows ? current : rows);
32642
+ }, []);
32643
+ const composerEditorCap = composerMaxRows(terminalRows);
32644
+ const dynamicRows = Math.max(1, terminalRows - 13 - composerGutterRows - (composerRows - 1));
31386
32645
  const streamingActive = view.streaming !== "" || view.streamingReasoning !== "";
31387
32646
  const deepDivingVisible = busy && !streamingActive;
31388
- const allLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2))), [
32647
+ const allLiveLines = (0, import_react.useMemo)(() => view.entries.slice(settled).flatMap((entry) => transcriptEntryLines(entry, Math.max(10, terminalColumns - 2), showReasoning)), [
31389
32648
  view.entries,
31390
32649
  settled,
31391
- terminalColumns
32650
+ terminalColumns,
32651
+ showReasoning
31392
32652
  ]);
31393
- const liveBudget = streamingActive ? Math.max(1, Math.floor(dynamicRows / 3)) : Math.max(0, dynamicRows - (deepDivingVisible ? 1 : 0));
32653
+ const liveBudget = busy || streamingActive ? Math.max(1, Math.floor(dynamicRows / 3)) : Math.max(0, dynamicRows - (deepDivingVisible ? 1 : 0));
31394
32654
  const visibleLiveLines = liveBudget === 0 ? [] : allLiveLines.slice(-liveBudget);
31395
32655
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
31396
32656
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
31397
32657
  const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
31398
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
31399
- const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || verboseOpen && !approvalPending && !questionPending || approvalPending || questionPending;
32658
+ const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !jobsOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !todosOpen && !verboseOpen && diffView === void 0 && !approvalPending && !questionPending;
32659
+ const modalVisible = modelOpen || helpOpen || modeOpen || permissionOpen || resumeOpen || pluginOpen || jobsOpen || statuslineOpen || themeOpen || historyOpen || agentsOpen || subagentOpen || todosOpen || verboseOpen && !approvalPending && !questionPending || diffView !== void 0 || approvalPending || questionPending;
31400
32660
  const closeInspector = (0, import_react.useCallback)(() => {
31401
32661
  setVerboseOpen(false);
31402
32662
  }, []);
31403
32663
  const refreshScreen = () => {
31404
- if (appStdout !== void 0) appStdout.write("\x1B[2J\x1B[3J\x1B[H");
32664
+ if (appStdout !== void 0) {
32665
+ synchronizedReplayPending.current = true;
32666
+ appStdout.write("\x1B[?2026h\x1B[r\x1B[0m\x1B[H\x1B[2J\x1B[3J\x1B[H");
32667
+ }
31405
32668
  setRefreshEpoch((epoch) => epoch + 1);
31406
32669
  };
32670
+ (0, import_react.useEffect)(() => {
32671
+ if (!synchronizedReplayPending.current || appStdout === void 0) return;
32672
+ synchronizedReplayPending.current = false;
32673
+ appStdout.write(SYNCHRONIZED_UPDATE_END);
32674
+ }, [appStdout, refreshEpoch]);
32675
+ const settledNeedsTrim = settledRowsCache.current?.needsTrim === true;
32676
+ (0, import_react.useEffect)(() => {
32677
+ if (!settledNeedsTrim || busy || streamingActive) return;
32678
+ refreshScreen();
32679
+ }, [
32680
+ settledNeedsTrim,
32681
+ busy,
32682
+ streamingActive
32683
+ ]);
31407
32684
  /** Apply one /model pick: record the selection, close the panel, report via notice. */
31408
32685
  const applyModel = (row, effortId) => {
31409
32686
  try {
@@ -31430,7 +32707,20 @@ function App(props) {
31430
32707
  };
31431
32708
  let modelSurface;
31432
32709
  if (modelOpen && !approvalPending && !questionPending) {
31433
- if (providerAction?.kind === "credential" && props.saveModelProviderCredential !== void 0) modelSurface = (0, import_react.createElement)(ProviderCredentialPanel, {
32710
+ if (providerAction?.kind === "configure" && props.saveModelProviderConfiguration !== void 0) modelSurface = (0, import_react.createElement)(ProviderConfigurationPanel, {
32711
+ target: providerAction.target,
32712
+ catalog: directory?.rows ?? [],
32713
+ save: props.saveModelProviderConfiguration,
32714
+ done: () => {
32715
+ const target = providerAction.target;
32716
+ setProviderAction(void 0);
32717
+ setProviderOpen(true);
32718
+ reloadModelSurfaces();
32719
+ notify(`provider configuration saved: ${target.displayName}`);
32720
+ },
32721
+ back: () => setProviderAction(void 0)
32722
+ });
32723
+ else if (providerAction?.kind === "credential" && props.saveModelProviderCredential !== void 0) modelSurface = (0, import_react.createElement)(ProviderCredentialPanel, {
31434
32724
  target: providerAction.target,
31435
32725
  save: props.saveModelProviderCredential,
31436
32726
  done: () => {
@@ -31481,6 +32771,16 @@ function App(props) {
31481
32771
  target
31482
32772
  });
31483
32773
  },
32774
+ onConfigure: (target) => {
32775
+ if (props.saveModelProviderConfiguration === void 0) {
32776
+ notify("provider configuration is unavailable in this profile", "warning");
32777
+ return;
32778
+ }
32779
+ setProviderAction({
32780
+ kind: "configure",
32781
+ target
32782
+ });
32783
+ },
31484
32784
  onUnset: (target) => {
31485
32785
  if (props.unsetModelProviderCredential === void 0) {
31486
32786
  notify("API key removal is unavailable in this profile", "warning");
@@ -31531,11 +32831,8 @@ function App(props) {
31531
32831
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
31532
32832
  key: refreshEpoch,
31533
32833
  items: settledRows
31534
- }), transcriptVisible ? (0, import_react.createElement)(Box, {
31535
- flexDirection: "column",
31536
- paddingX: 2
31537
- }, visibleLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: visibleLiveLines }), view.streamingReasoning !== "" && reasoningRows > 0 ? (0, import_react.createElement)(StreamTail, {
31538
- text: showReasoning ? view.streamingReasoning : "Thinking…",
32834
+ }), 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, {
32835
+ text: showReasoning ? view.streamingReasoning : "Thinking… (Ctrl+R to expand)",
31539
32836
  prefix: "✻ ",
31540
32837
  continuationPrefix: " ",
31541
32838
  dim: true,
@@ -31545,7 +32842,12 @@ function App(props) {
31545
32842
  dim: false,
31546
32843
  maxRows: answerRows,
31547
32844
  prefix: " "
31548
- }, 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, (0, import_react.createElement)(QuestionBar, {
32845
+ }, 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, {
32846
+ todos: view.todos,
32847
+ onClose: () => {
32848
+ setTodosOpen(false);
32849
+ }
32850
+ }) : void 0, (0, import_react.createElement)(QuestionBar, {
31549
32851
  store: props.questions,
31550
32852
  snapshot: questionSnapshot,
31551
32853
  locked: false
@@ -31561,6 +32863,9 @@ function App(props) {
31561
32863
  onClose: () => {
31562
32864
  setHelpOpen(false);
31563
32865
  }
32866
+ }) : void 0, diffView !== void 0 && !approvalPending && !questionPending ? (0, import_react.createElement)(DiffPanel, {
32867
+ view: diffView,
32868
+ onClose: () => setDiffView(void 0)
31564
32869
  }) : void 0, verboseOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoVerbosePanel, {
31565
32870
  entries: view.entries,
31566
32871
  onClose: closeInspector
@@ -31604,6 +32909,9 @@ function App(props) {
31604
32909
  load: props.loadPlugins,
31605
32910
  initialQuery: pluginQuery,
31606
32911
  close: () => setPluginOpen(false)
32912
+ }) : void 0, jobsOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(JobsPanel, {
32913
+ load: props.loadJobs,
32914
+ close: () => setJobsOpen(false)
31607
32915
  }) : void 0, statuslineOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(StatuslinePanel, {
31608
32916
  enabled: statuslineItems,
31609
32917
  change: (items) => {
@@ -31717,11 +33025,13 @@ function App(props) {
31717
33025
  setPluginQuery(query);
31718
33026
  setPluginOpen(true);
31719
33027
  },
33028
+ openJobs: () => setJobsOpen(true),
31720
33029
  openStatusline: () => setStatuslineOpen(true),
31721
33030
  openTheme: () => setThemeOpen(true),
31722
33031
  openHistory: () => setHistoryOpen(true),
31723
33032
  openAgents: () => setAgentsOpen(true),
31724
33033
  openSubagent: () => setSubagentOpen(true),
33034
+ openTodos: () => setTodosOpen(true),
31725
33035
  openDelete: (id) => {
31726
33036
  const armed = id === void 0 || id === "" ? void 0 : id;
31727
33037
  setResumeDelete({
@@ -31731,10 +33041,17 @@ function App(props) {
31731
33041
  setDeleteConfirmId(armed);
31732
33042
  setResumeOpen(true);
31733
33043
  },
33044
+ openDiff: (argument) => {
33045
+ props.loadGitDiff(argument).then(setDiffView, (error) => {
33046
+ notify(`diff failed: ${error instanceof Error ? error.message : String(error)}`, "error");
33047
+ });
33048
+ },
33049
+ reviewChanges: props.reviewChanges,
31734
33050
  deleteConfirm: deleteConfirmId,
31735
33051
  confirmDelete,
31736
33052
  cancelDelete,
31737
33053
  createSession: props.createSession,
33054
+ forkSession: props.forkSession,
31738
33055
  cancelSessionSwitch: props.cancelSessionSwitch,
31739
33056
  notify,
31740
33057
  hasNotice: notice !== void 0,
@@ -31750,12 +33067,13 @@ function App(props) {
31750
33067
  refresh: refreshScreen,
31751
33068
  toggleReasoning: () => {
31752
33069
  setShowReasoning((current) => !current);
31753
- refreshScreen();
33070
+ if (!busy && !streamingActive) refreshScreen();
31754
33071
  },
31755
33072
  loadMentions: props.loadMentions,
31756
33073
  cyclePermission: props.cyclePermission,
31757
33074
  exportTranscript: props.exportTranscript,
31758
33075
  renameTitle: props.renameTitle,
33076
+ copyLastResponse: props.copyLastResponse,
31759
33077
  recallSpace,
31760
33078
  recordLocal,
31761
33079
  recordHistory: props.recordHistory,
@@ -31764,7 +33082,9 @@ function App(props) {
31764
33082
  historyFill,
31765
33083
  historyConsumed,
31766
33084
  waveTier,
31767
- waveStyle
33085
+ waveStyle,
33086
+ maxRows: composerEditorCap,
33087
+ onEditorRows: handleEditorRows
31768
33088
  }), (0, import_react.createElement)(StatusLine, {
31769
33089
  facts: {
31770
33090
  model: modelLabel,
@@ -31951,13 +33271,15 @@ function isSlashLine(line) {
31951
33271
  /** Substitutable runner effects; production values write to the real terminal. */
31952
33272
  const internals = {
31953
33273
  mount: (element) => {
31954
- const instance = render(element);
33274
+ process.stdout.write("\x1B[>4;0m\x1B[>5u\x1B[?2004h");
33275
+ const instance = render(element, { exitOnCtrlC: false });
31955
33276
  return {
31956
33277
  rerender(element) {
31957
33278
  instance.rerender(element);
31958
33279
  },
31959
33280
  unmount() {
31960
33281
  instance.unmount();
33282
+ process.stdout.write("\x1B[<u\x1B[>4;0m\x1B[?2004l");
31961
33283
  }
31962
33284
  };
31963
33285
  },
@@ -32148,6 +33470,26 @@ function profileRefOf(profile) {
32148
33470
  const ref = profile.apiKeyEnv;
32149
33471
  return typeof ref === "string" && ref.length > 0 ? ref : void 0;
32150
33472
  }
33473
+ /** Extract only fields the terminal can round-trip without touching provider-specific extras. */
33474
+ function configurationOf(profile) {
33475
+ if (typeof profile !== "object" || profile === null) return { models: [] };
33476
+ const record = profile;
33477
+ const models = (Array.isArray(record.models) ? record.models : []).flatMap((value) => {
33478
+ if (typeof value !== "object" || value === null) return [];
33479
+ const entry = value;
33480
+ if (typeof entry.id !== "string" || entry.id.trim() === "") return [];
33481
+ return [{
33482
+ id: entry.id,
33483
+ ...typeof entry.name === "string" && entry.name.trim() !== "" ? { name: entry.name } : {},
33484
+ ...typeof entry.contextWindow === "number" && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {},
33485
+ ...typeof entry.maxTokens === "number" && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {}
33486
+ }];
33487
+ });
33488
+ return {
33489
+ ...typeof record.baseURL === "string" && record.baseURL.trim() !== "" ? { baseURL: record.baseURL } : {},
33490
+ models
33491
+ };
33492
+ }
32151
33493
  /**
32152
33494
  * The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
32153
33495
  * with the route uppercased and every non-alphanumeric run collapsed to one
@@ -32250,6 +33592,7 @@ async function loadProviderSettings(ctx) {
32250
33592
  settingsRevision: namespace?.revision ?? 0,
32251
33593
  configured,
32252
33594
  removable,
33595
+ configuration: configurationOf(profile),
32253
33596
  ...credentialRef === void 0 ? {} : { credentialRef },
32254
33597
  suggestedRef: deriveCredentialRef(base.provider),
32255
33598
  ...base.declared === void 0 ? {} : { declared: base.declared }
@@ -32335,6 +33678,50 @@ async function saveProviderCredential(ctx, target, rawKey) {
32335
33678
  throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
32336
33679
  }
32337
33680
  }
33681
+ /** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
33682
+ async function saveProviderConfiguration(ctx, target, configuration) {
33683
+ if (target.settingsNs.length === 0) throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
33684
+ const settings = ctx.get("settings");
33685
+ if (settings === void 0 || settings.writable !== true) throw new ProviderSettingsError("settings are read-only; provider configuration cannot be changed here");
33686
+ const baseURL = configuration.baseURL?.trim();
33687
+ if (baseURL !== void 0 && baseURL !== "") try {
33688
+ const parsed = new URL(baseURL);
33689
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
33690
+ } catch {
33691
+ throw new ProviderSettingsError("base URL must be an absolute http or https URL");
33692
+ }
33693
+ const seen = /* @__PURE__ */ new Set();
33694
+ const models = configuration.models.map((model) => {
33695
+ const id = model.id.trim();
33696
+ if (id === "" || seen.has(id)) throw new ProviderSettingsError("each selected model must have a unique non-empty id");
33697
+ seen.add(id);
33698
+ 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`);
33699
+ return {
33700
+ id,
33701
+ ...model.name === void 0 || model.name.trim() === "" ? {} : { name: model.name.trim() },
33702
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
33703
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
33704
+ };
33705
+ });
33706
+ const root = target.settingsPath;
33707
+ const ops = [baseURL === void 0 || baseURL === "" ? {
33708
+ op: "unset",
33709
+ path: [...root, "baseURL"]
33710
+ } : {
33711
+ op: "set",
33712
+ path: [...root, "baseURL"],
33713
+ value: baseURL
33714
+ }, {
33715
+ op: "set",
33716
+ path: [...root, "models"],
33717
+ value: models
33718
+ }];
33719
+ try {
33720
+ await settings.mutate(target.settingsNs, ops, target.settingsRevision);
33721
+ } catch (error) {
33722
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
33723
+ }
33724
+ }
32338
33725
  /**
32339
33726
  * Remove the currently named credential without touching the provider
32340
33727
  * profile. Only the resolved profile's own reference is unset; a dormant or
@@ -32395,133 +33782,43 @@ async function removeProviderSettings(ctx, target) {
32395
33782
  }
32396
33783
  //#endregion
32397
33784
  //#region src/mentions.ts
32398
- /**
32399
- * Workspace @mention support: file and directory candidates from a bounded
32400
- * async scan of the session cwd, session candidates from the opt-in
32401
- * `sessionReferenceResolver` service, and submission preparation through its
32402
- * `prepare()` API. Picked session mentions land as canonical
32403
- * `@[label](dsh-session:…)` tokens; on submit the text is parsed back into
32404
- * readable `@label` text plus structured references, snapshots are injected
32405
- * via `agent.inject()` before the readable message wakes the driver
32406
- * (`followup` idle, `steer` running) — exactly the upstream README's wiring.
32407
- *
32408
- * Harness exposes no workspace-file mention service (only session references
32409
- * plus a post-hoc produced-file linker), so the file index is the lightweight
32410
- * bounded scan below, kept deliberately smaller than Codex's streaming
32411
- * gitignore-aware walker.
32412
- *
32413
- * @module @deepseek-ai/dsh-code/mentions
32414
- */
32415
- /** Directories never entered and files never listed during the scan. */
32416
- const SKIP_DIRS = /* @__PURE__ */ new Set([
32417
- ".git",
32418
- "node_modules",
32419
- "lib",
32420
- "dist",
32421
- "out",
32422
- ".omc",
32423
- "coverage"
32424
- ]);
32425
- const MAX_FILES = 4e3;
32426
- const MAX_DEPTH = 12;
32427
- /** Empty-query default rows: proves the index exists without typing (Codex's
32428
- * popups show something on a bare sigil too). */
32429
- const EMPTY_QUERY_ROWS = 20;
32430
- /**
32431
- * Bounded async BFS scan of a workspace; unreadable entries are skipped.
32432
- * Both files and directories are indexed (directories insert with a trailing
32433
- * slash), mirroring Codex's `MatchType::{File,Directory}` index. Dotfiles and
32434
- * the {@link SKIP_DIRS} list are excluded, which is a coarser filter than
32435
- * Codex's gitignore-aware walker but stays dependency-free and bounded.
32436
- */
32437
- async function scanWorkspaceFiles(root, signal) {
32438
- const found = [];
32439
- const pending = [{
32440
- absolute: root,
32441
- relative: "",
32442
- depth: 0
32443
- }];
32444
- const aborted = () => signal?.aborted === true;
32445
- while (pending.length > 0 && found.length < MAX_FILES && !aborted()) {
32446
- const current = pending.shift();
32447
- if (current === void 0) break;
32448
- let entries;
32449
- try {
32450
- entries = await readdir(current.absolute, { withFileTypes: true });
32451
- } catch {
32452
- continue;
32453
- }
32454
- for (const entry of entries) {
32455
- if (found.length >= MAX_FILES || aborted()) return found;
32456
- if (entry.name.startsWith(".")) continue;
32457
- const relative = current.relative === "" ? entry.name : `${current.relative}/${entry.name}`;
32458
- if (entry.isDirectory()) {
32459
- if (SKIP_DIRS.has(entry.name) || current.depth + 1 > MAX_DEPTH) continue;
32460
- found.push({
32461
- path: relative,
32462
- kind: "directory"
32463
- });
32464
- pending.push({
32465
- absolute: join(current.absolute, entry.name),
32466
- relative,
32467
- depth: current.depth + 1
32468
- });
32469
- } else if (entry.isFile()) found.push({
32470
- path: relative,
32471
- kind: "file"
32472
- });
32473
- }
32474
- }
32475
- return found.sort((left, right) => left.path < right.path ? -1 : 1);
32476
- }
32477
- /** True when every query character appears in order in the haystack. */
32478
- function isSubsequence(query, haystack) {
32479
- let at = 0;
32480
- for (const char of haystack) {
32481
- if (char === query[at]) at += 1;
32482
- if (at >= query.length) return true;
32483
- }
32484
- return at >= query.length;
32485
- }
32486
- /** Rank one file path against the typed query (community-TUI scoring shape). */
32487
- function scoreFile(path, query) {
32488
- const name = path.slice(path.lastIndexOf("/") + 1);
32489
- if (query === "") return 0;
32490
- if (name === query) return 1e3;
32491
- if (name.startsWith(query)) return 900;
32492
- if (name.includes(query)) return 700;
32493
- if (path.includes(query)) return 500;
32494
- if (isSubsequence(query, name)) return 300;
32495
- return 0;
32496
- }
33785
+ /** Menu cap on file rows; the service owns ranking and default rows. */
33786
+ const MAX_FILE_ROWS = 20;
32497
33787
  /**
32498
33788
  * Create the mention API for one agent's workspace. A missing
32499
- * session-reference service degrades to file mentions only (the scan still
32500
- * works); `prepare` then passes text through untouched. An undefined agent
32501
- * (a bare launch before any session exists) also degrades to file-only
32502
- * mentions, so `@` file completion works before the first message.
33789
+ * `fileReferences` service (with an agent present) or `sessionReferenceResolver`
33790
+ * degrades that half to empty rows; `prepare` passes text through untouched
33791
+ * without references. An undefined agent (a bare launch before any session
33792
+ * exists) runs the official WorkspaceFileSearch over the launch cwd — the
33793
+ * same class the mounted service uses per agent — so `@` file completion
33794
+ * works from the first keystroke; session references wait for the session.
32503
33795
  *
32504
- * `files` is hoisted into the closure so `candidates` never reaches for
32505
- * `this` the runner hands `mentions.candidates` to the input editor as a
32506
- * detached callback, and a `this`-bound method would throw on every `@` key.
32507
- * @param ctx - context carrying the optional `sessionReferenceResolver`.
32508
- * @param agent - the session owner; excluded from its own candidates.
32509
- * @param cwd - workspace root to scan.
33796
+ * `candidates` never reaches for `this` — the runner hands it to the input
33797
+ * editor as a detached callback, and a `this`-bound method would throw on
33798
+ * every `@` key.
33799
+ * @param ctx - context carrying the optional `fileReferences` and
33800
+ * `sessionReferenceResolver` services.
33801
+ * @param agent - the session owner; excluded from its own session candidates.
33802
+ * @param cwd - launch working directory; bounds the pre-session search.
32510
33803
  */
32511
33804
  function createMentions(ctx, agent, cwd) {
32512
33805
  const resolver = ctx.get("sessionReferenceResolver");
33806
+ const fileReferences = ctx.get("fileReferences");
32513
33807
  const sessionCapable = agent !== void 0 && resolver !== void 0;
32514
- let filesPromise;
32515
- const files = () => {
32516
- filesPromise ??= scanWorkspaceFiles(cwd);
32517
- return filesPromise;
33808
+ let preSessionSearch;
33809
+ const preSessionFiles = (query, signal) => {
33810
+ preSessionSearch ??= new WorkspaceFileSearch(cwd, {
33811
+ maxResults: DEFAULT_FILE_SEARCH_MAX_RESULTS,
33812
+ maxEntries: DEFAULT_FILE_SEARCH_MAX_ENTRIES,
33813
+ excludedDirectories: [...DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES]
33814
+ });
33815
+ return preSessionSearch.list(query, signal ?? new AbortController().signal);
32518
33816
  };
32519
33817
  return {
32520
- files,
32521
33818
  async candidates(query, signal) {
32522
33819
  const needle = query.trim();
32523
- const [scanned, sessions] = await Promise.all([files(), sessionCapable && needle !== "" && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
32524
- const fileRows = (needle === "" ? scanned.slice(0, EMPTY_QUERY_ROWS) : scanned.filter((candidate) => scoreFile(candidate.path, needle) > 0).sort((left, right) => scoreFile(right.path, needle) - scoreFile(left.path, needle)).slice(0, 20)).map((candidate) => ({
33820
+ 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([])]);
33821
+ const fileRows = files.slice(0, MAX_FILE_ROWS).map((candidate) => ({
32525
33822
  label: candidate.path,
32526
33823
  description: candidate.kind === "directory" ? "Folder" : "File",
32527
33824
  kind: candidate.kind
@@ -32647,6 +33944,8 @@ function mountQuestionProvider(ctx) {
32647
33944
  }
32648
33945
  //#endregion
32649
33946
  //#region src/store.ts
33947
+ /** Render frame budget: the notification cadence's upper bound. */
33948
+ const NOTIFY_FRAME_MS$1 = 16;
32650
33949
  /**
32651
33950
  * Create one transcript store, optionally seeded with replayed history. The
32652
33951
  * seed folds synchronously BEFORE the first render, so a resumed session
@@ -32661,13 +33960,18 @@ function createTranscriptStore(replay) {
32661
33960
  let view = replay === void 0 ? createTranscriptView() : projectEvents(replay);
32662
33961
  const listeners = /* @__PURE__ */ new Set();
32663
33962
  let scheduled = false;
33963
+ let lastNotifyAt = 0;
32664
33964
  const notify = () => {
32665
33965
  if (scheduled) return;
32666
33966
  scheduled = true;
32667
- queueMicrotask(() => {
33967
+ const wait = NOTIFY_FRAME_MS$1 - (Date.now() - lastNotifyAt);
33968
+ const dispatch = () => {
32668
33969
  scheduled = false;
33970
+ lastNotifyAt = Date.now();
32669
33971
  for (const listener of listeners) listener();
32670
- });
33972
+ };
33973
+ if (wait <= 0) setImmediate(dispatch);
33974
+ else setTimeout(dispatch, wait);
32671
33975
  };
32672
33976
  return {
32673
33977
  getView: () => view,
@@ -32689,6 +33993,8 @@ function createTranscriptStore(replay) {
32689
33993
  }
32690
33994
  };
32691
33995
  }
33996
+ /** Render frame budget: the notification cadence's upper bound. */
33997
+ const NOTIFY_FRAME_MS = 16;
32692
33998
  /** Bounded last-activity text (plain characters, display-sliced later). */
32693
33999
  const MAX_ACTIVITY_CHARS = 80;
32694
34000
  /** Single-line bounded preview of an assembled message's text content. */
@@ -32802,13 +34108,18 @@ function createSubagentFeed() {
32802
34108
  let rows = Object.freeze([]);
32803
34109
  const listeners = /* @__PURE__ */ new Set();
32804
34110
  let scheduled = false;
34111
+ let lastNotifyAt = 0;
32805
34112
  const notify = () => {
32806
34113
  if (scheduled) return;
32807
34114
  scheduled = true;
32808
- queueMicrotask(() => {
34115
+ const wait = NOTIFY_FRAME_MS - (Date.now() - lastNotifyAt);
34116
+ const dispatch = () => {
32809
34117
  scheduled = false;
34118
+ lastNotifyAt = Date.now();
32810
34119
  for (const listener of listeners) listener();
32811
- });
34120
+ };
34121
+ if (wait <= 0) setImmediate(dispatch);
34122
+ else setTimeout(dispatch, wait);
32812
34123
  };
32813
34124
  return {
32814
34125
  apply(sessionId, event) {
@@ -32926,7 +34237,7 @@ function buildExportMarkdown(view, sessionId) {
32926
34237
  for (const entry of view.entries) switch (entry.kind) {
32927
34238
  case "user":
32928
34239
  if (entry.notice) out.push(`> ⤷ context: ${entry.text}`, "");
32929
- else out.push("## user", "", entry.text, "");
34240
+ else out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
32930
34241
  break;
32931
34242
  case "assistant":
32932
34243
  if (entry.reasoning !== "") out.push("<details><summary>thinking</summary>", "", entry.reasoning, "", "</details>", "");
@@ -32959,7 +34270,7 @@ function buildExportMarkdown(view, sessionId) {
32959
34270
  out.push(`> files changed: ${entry.paths.join(", ")}`, "");
32960
34271
  break;
32961
34272
  case "pending":
32962
- out.push("## user", "", entry.text, "");
34273
+ out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
32963
34274
  break;
32964
34275
  default: assertNever(entry, "transcript entry kind");
32965
34276
  }
@@ -32973,6 +34284,212 @@ function buildExportMarkdown(view, sessionId) {
32973
34284
  return out.join("\n");
32974
34285
  }
32975
34286
  //#endregion
34287
+ //#region src/attachments.ts
34288
+ /** Terminal image-file adapter over the Harness durable attachment service. */
34289
+ /** Detect the supported encoded raster formats from bytes, never from a path suffix. */
34290
+ function detectImageMediaType(data) {
34291
+ if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
34292
+ if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
34293
+ if (data.length >= 6) {
34294
+ const signature = String.fromCharCode(...data.subarray(0, 6));
34295
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
34296
+ }
34297
+ if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
34298
+ }
34299
+ /** Read, validate, and persist an ordered image path list as model content blocks. */
34300
+ async function saveImagePaths(paths, attachments) {
34301
+ if (paths.length === 0) return [];
34302
+ if (attachments === void 0) throw new Error("image attachments are unavailable in this profile");
34303
+ const inputs = [];
34304
+ for (const path of paths) {
34305
+ let data;
34306
+ try {
34307
+ data = await readFile(path);
34308
+ } catch (error) {
34309
+ throw new Error(`cannot read image "${path}": ${error instanceof Error ? error.message : String(error)}`);
34310
+ }
34311
+ const mediaType = detectImageMediaType(data);
34312
+ if (mediaType === void 0) throw new Error(`unsupported image file "${path}" (expected PNG, JPEG, WebP, or GIF)`);
34313
+ inputs.push({
34314
+ data,
34315
+ mediaType,
34316
+ name: basename(path)
34317
+ });
34318
+ }
34319
+ return (await attachments.saveImages(inputs)).map((attachment) => ({
34320
+ type: "image",
34321
+ attachment
34322
+ }));
34323
+ }
34324
+ //#endregion
34325
+ //#region src/editor.ts
34326
+ /** Host editor and clipboard adapters used by the terminal surface. */
34327
+ function waitForProcess(command, args, input) {
34328
+ return new Promise((resolve, reject) => {
34329
+ const child = spawn(command, [...args], {
34330
+ stdio: input === void 0 ? "inherit" : [
34331
+ "pipe",
34332
+ "ignore",
34333
+ "pipe"
34334
+ ],
34335
+ windowsHide: true
34336
+ });
34337
+ let stderr = "";
34338
+ child.stderr?.on("data", (chunk) => {
34339
+ stderr += String(chunk);
34340
+ });
34341
+ child.once("error", reject);
34342
+ child.once("exit", (code) => {
34343
+ if (code === 0) resolve();
34344
+ else reject(/* @__PURE__ */ new Error(`${command} exited with code ${String(code)}${stderr.trim() === "" ? "" : `: ${stderr.trim()}`}`));
34345
+ });
34346
+ if (input !== void 0) child.stdin?.end(input);
34347
+ });
34348
+ }
34349
+ /** Copy UTF-8 text through the platform clipboard command. */
34350
+ async function copyText(text) {
34351
+ if (process.platform === "win32") {
34352
+ await waitForProcess("powershell.exe", [
34353
+ "-NoProfile",
34354
+ "-NonInteractive",
34355
+ "-Command",
34356
+ "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); Set-Clipboard -Value ([Console]::In.ReadToEnd())"
34357
+ ], text);
34358
+ return;
34359
+ }
34360
+ if (process.platform === "darwin") {
34361
+ await waitForProcess("pbcopy", [], text);
34362
+ return;
34363
+ }
34364
+ await waitForProcess("xclip", ["-selection", "clipboard"], text);
34365
+ }
34366
+ /** Latest complete assistant text, excluding streaming and reasoning. */
34367
+ function latestAssistantText(view) {
34368
+ for (let index = view.entries.length - 1; index >= 0; index -= 1) {
34369
+ const entry = view.entries[index];
34370
+ if (entry?.kind === "assistant" && entry.text !== "") return entry.text;
34371
+ }
34372
+ }
34373
+ //#endregion
34374
+ //#region src/fork.ts
34375
+ /** Select a completed turn and trailing between-turn metadata. */
34376
+ function selectForkSeed(events, atSeq) {
34377
+ if (atSeq !== void 0 && (!Number.isSafeInteger(atSeq) || atSeq < 0)) throw new Error("fork event sequence must be a non-negative integer");
34378
+ const lastSeq = events.at(-1)?.seq ?? -1;
34379
+ const boundary = (atSeq === void 0 ? void 0 : events.find((event) => event.type === "turn/end" && event.seq >= atSeq)) ?? (atSeq === void 0 || atSeq > lastSeq ? events.findLast((event) => event.type === "turn/end") : void 0);
34380
+ if (boundary === void 0) throw new Error(atSeq !== void 0 && atSeq <= lastSeq ? `the turn containing event ${atSeq} has not completed` : "this session has no completed turn to fork from");
34381
+ let cut = events.indexOf(boundary) + 1;
34382
+ while (cut < events.length && events[cut]?.type !== "turn/start") cut += 1;
34383
+ return {
34384
+ boundarySeq: boundary.seq,
34385
+ events: events.slice(0, cut)
34386
+ };
34387
+ }
34388
+ //#endregion
34389
+ //#region src/git-workflow.ts
34390
+ /** Read-only Git inspection used by /diff and /review. */
34391
+ /** Split Git's stable `diff --git` framing without interpreting patch content. */
34392
+ function parseGitDiffFiles(text) {
34393
+ if (text === "") return [];
34394
+ return text.split(/(?=^diff --git )/mu).filter((chunk) => chunk !== "").map((chunk, index) => {
34395
+ const lines = chunk.replace(/\n$/u, "").split("\n");
34396
+ const plus = lines.find((line) => line.startsWith("+++ b/"));
34397
+ const minus = lines.find((line) => line.startsWith("--- a/"));
34398
+ const header = /^diff --git a\/(.+) b\/(.+)$/u.exec(lines[0] ?? "");
34399
+ return {
34400
+ path: plus?.slice(6) || minus?.slice(6) || header?.[2] || header?.[1] || `file ${index + 1}`,
34401
+ lines
34402
+ };
34403
+ });
34404
+ }
34405
+ /** Parse the intentionally small, option-safe /diff argument vocabulary. */
34406
+ function parseGitDiffSpec(argument) {
34407
+ const value = argument.trim();
34408
+ if (value === "") return {
34409
+ label: "working tree vs HEAD",
34410
+ args: [
34411
+ "diff",
34412
+ "--no-ext-diff",
34413
+ "--unified=3",
34414
+ "HEAD",
34415
+ "--"
34416
+ ]
34417
+ };
34418
+ if (value === "--staged" || value === "--cached") return {
34419
+ label: "staged changes",
34420
+ args: [
34421
+ "diff",
34422
+ "--no-ext-diff",
34423
+ "--unified=3",
34424
+ "--cached",
34425
+ "--"
34426
+ ]
34427
+ };
34428
+ if (value.startsWith("-") || /\s/u.test(value)) throw new Error("usage: /diff [--staged|git-ref]");
34429
+ return {
34430
+ label: `changes since ${value}`,
34431
+ args: [
34432
+ "diff",
34433
+ "--no-ext-diff",
34434
+ "--unified=3",
34435
+ value,
34436
+ "--"
34437
+ ]
34438
+ };
34439
+ }
34440
+ function executeGit(cwd, args) {
34441
+ return new Promise((resolve, reject) => {
34442
+ execFile("git", [...args], {
34443
+ cwd,
34444
+ encoding: "utf8",
34445
+ maxBuffer: 16777216,
34446
+ windowsHide: true
34447
+ }, (error, stdout, stderr) => {
34448
+ if (error !== null) {
34449
+ reject(new Error(stderr.trim() || error.message));
34450
+ return;
34451
+ }
34452
+ resolve(stdout.replace(/\r\n/gu, "\n"));
34453
+ });
34454
+ });
34455
+ }
34456
+ /** Load one complete textual diff without invoking external diff drivers. */
34457
+ async function loadGitDiff(cwd, argument) {
34458
+ const spec = parseGitDiffSpec(argument);
34459
+ try {
34460
+ const text = await executeGit(cwd, spec.args);
34461
+ return {
34462
+ title: `git diff - ${spec.label}`,
34463
+ files: parseGitDiffFiles(text)
34464
+ };
34465
+ } catch (error) {
34466
+ if (argument.trim() !== "") throw error;
34467
+ return {
34468
+ title: "git diff - working tree",
34469
+ files: parseGitDiffFiles(await executeGit(cwd, [
34470
+ "diff",
34471
+ "--no-ext-diff",
34472
+ "--unified=3",
34473
+ "--"
34474
+ ]))
34475
+ };
34476
+ }
34477
+ }
34478
+ /** Review prompt capped before it reaches a provider context window. */
34479
+ function buildReviewPrompt(diff, label, maxChars = 2e5) {
34480
+ const truncated = diff.length > maxChars;
34481
+ const body = truncated ? diff.slice(0, maxChars) : diff;
34482
+ return [
34483
+ "Review the following Git changes. Do not modify files or run write operations.",
34484
+ "Lead with concrete bugs, regressions, security risks, and missing tests, ordered by severity.",
34485
+ `Scope: ${label}${truncated ? " (diff truncated by CLI)" : ""}`,
34486
+ "",
34487
+ "```diff",
34488
+ body,
34489
+ "```"
34490
+ ].join("\n");
34491
+ }
34492
+ //#endregion
32976
34493
  //#region src/session-switch.ts
32977
34494
  var SessionSwitchQueue = class {
32978
34495
  execute;
@@ -33146,7 +34663,9 @@ const Config = z.object({ startup: z.object({
33146
34663
  kind: z.string().required(),
33147
34664
  sessionId: z.string(),
33148
34665
  mode: z.string(),
33149
- theme: z.string()
34666
+ theme: z.string(),
34667
+ prompt: z.string(),
34668
+ images: z.array(z.string())
33150
34669
  }) });
33151
34670
  /** Report an unexpected direct-driver failure and request a failing exit. */
33152
34671
  function fail(io, error) {
@@ -33154,6 +34673,33 @@ function fail(io, error) {
33154
34673
  io.exit(1);
33155
34674
  }
33156
34675
  /**
34676
+ * Snapshot caller-visible background jobs for the /jobs panel. Jobs the agent
34677
+ * started through run_in_background are fenced by their owner, so the CURRENT
34678
+ * agent is the caller. A missing registry is a harmless absence (the base
34679
+ * composition may not mount one) and collapses to the empty panel state —
34680
+ * the documented degradation for harmless probes, not an error.
34681
+ * @param ctx - context carrying the optional `jobs` registry.
34682
+ * @param caller - the active agent (undefined sees only unowned jobs).
34683
+ * @returns job rows in registration order; never throws.
34684
+ */
34685
+ function listJobs(ctx, caller) {
34686
+ const jobs = ctx.get("jobs");
34687
+ if (jobs === void 0) return [];
34688
+ try {
34689
+ return jobs.list(caller).map((job) => ({
34690
+ id: job.id,
34691
+ kind: job.kind,
34692
+ label: job.label,
34693
+ status: job.status,
34694
+ detail: job.detail,
34695
+ startedAt: job.startedAt,
34696
+ finishedAt: job.finishedAt
34697
+ }));
34698
+ } catch {
34699
+ return [];
34700
+ }
34701
+ }
34702
+ /**
33157
34703
  * Resolve the working directory's git branch for the status line.
33158
34704
  * @param cwd - the session's working directory.
33159
34705
  * @returns the branch name, or '' outside a repository or on a detached HEAD.
@@ -33318,21 +34864,23 @@ async function run(ctx, startup, io) {
33318
34864
  sessionId: SessionId(next.sessionId),
33319
34865
  meta: {
33320
34866
  cwd: nextCwd,
33321
- agentPreset: mode
34867
+ agentPreset: mode,
34868
+ ...next.parentSession === void 0 ? {} : { parentSession: next.parentSession },
34869
+ ...next.seedLength === void 0 ? {} : { seedLength: next.seedLength }
33322
34870
  },
34871
+ ...next.seed === void 0 ? {} : { seed: next.seed },
33323
34872
  agentOptions: seedOptions,
33324
34873
  signal: quitAbort.signal,
33325
34874
  setup
33326
34875
  });
33327
34876
  const session = handle.agent.session;
33328
34877
  if (!next.resume && permissionPresets !== void 0) applyPendingPermission(permissionPresets, session, pendingPermission);
33329
- const sessionCwd = session.header.cwd ?? nextCwd;
33330
34878
  return {
33331
34879
  handle,
33332
34880
  agent: handle.agent,
33333
34881
  session,
33334
34882
  store: createTranscriptStore(session.events),
33335
- mentions: createMentions(ctx, handle.agent, sessionCwd),
34883
+ mentions: createMentions(ctx, handle.agent, session.header.cwd ?? nextCwd),
33336
34884
  mode: mode ?? "standard",
33337
34885
  selection: selectionState,
33338
34886
  resumed: next.resume
@@ -33398,7 +34946,7 @@ async function run(ctx, startup, io) {
33398
34946
  store.apply(event);
33399
34947
  return;
33400
34948
  }
33401
- if (subject.header.parentSession === session.id) subagents.apply(subject.id, event);
34949
+ if (subject.header.parentSession === session.id && subject.header.origin === "subagent") subagents.apply(subject.id, event);
33402
34950
  });
33403
34951
  const commands = watchCommands(ctx);
33404
34952
  if (agent !== void 0) commands.setAgent(agent);
@@ -33524,7 +35072,7 @@ async function run(ctx, startup, io) {
33524
35072
  const finish = () => {
33525
35073
  pendingControllers.delete(controller);
33526
35074
  };
33527
- Promise.resolve().then(() => registry.execute(currentAgent, line, controller.signal)).then((execution) => {
35075
+ Promise.resolve().then(() => registry.execute(currentAgent, line, [], controller.signal)).then((execution) => {
33528
35076
  finish();
33529
35077
  if (epoch !== atEpoch || agent !== currentAgent) return;
33530
35078
  if (execution === void 0) try {
@@ -33545,10 +35093,10 @@ async function run(ctx, startup, io) {
33545
35093
  });
33546
35094
  };
33547
35095
  /** Deliver one trimmed line to the live session, expanding mentions first. */
33548
- const deliverLine = (line, mode) => {
35096
+ const deliverLine = (line, mode, images = []) => {
33549
35097
  const currentAgent = agent;
33550
35098
  const currentMentions = mentions;
33551
- if (isSlashLine(line) && mode === "followup") {
35099
+ if (images.length === 0 && isSlashLine(line) && mode === "followup") {
33552
35100
  runSlash(line);
33553
35101
  return;
33554
35102
  }
@@ -33564,11 +35112,12 @@ async function run(ctx, startup, io) {
33564
35112
  if (epoch !== atEpoch || agent !== currentAgent) return;
33565
35113
  try {
33566
35114
  if (context !== void 0) currentAgent.inject(context);
35115
+ const content = [...readable === "" ? [] : [{
35116
+ type: "text",
35117
+ text: readable
35118
+ }], ...images];
33567
35119
  const message = createUserMessage({
33568
- content: [{
33569
- type: "text",
33570
- text: readable
33571
- }],
35120
+ content,
33572
35121
  source: { kind: "user" }
33573
35122
  });
33574
35123
  if (mode === "steer") currentAgent.steer(message);
@@ -33602,7 +35151,7 @@ async function run(ctx, startup, io) {
33602
35151
  await pendingModeWork;
33603
35152
  if (session !== void 0) {
33604
35153
  const queued = pendingInputs.splice(0);
33605
- for (const item of queued) deliverLine(item.text, item.mode);
35154
+ for (const item of queued) deliverLine(item.text, item.mode, item.images);
33606
35155
  return;
33607
35156
  }
33608
35157
  const next = await prepare({
@@ -33629,7 +35178,7 @@ async function run(ctx, startup, io) {
33629
35178
  abortPendingControllers();
33630
35179
  epoch += 1;
33631
35180
  const queued = pendingInputs.splice(0);
33632
- for (const item of queued) deliverLine(item.text, item.mode);
35181
+ for (const item of queued) deliverLine(item.text, item.mode, item.images);
33633
35182
  } finally {
33634
35183
  creating = false;
33635
35184
  }
@@ -33639,14 +35188,14 @@ async function run(ctx, startup, io) {
33639
35188
  });
33640
35189
  };
33641
35190
  /** Deliver one readable line to the agent, expanding session mentions first. */
33642
- const send = (text, mode) => {
35191
+ const send = (text, mode, images = []) => {
33643
35192
  const line = text.trim();
33644
- if (line === "") return;
33645
- if (line.startsWith("/mode ")) {
35193
+ if (line === "" && images.length === 0) return;
35194
+ if (images.length === 0 && line.startsWith("/mode ")) {
33646
35195
  switchModeAction(line.slice(6).trim()).then((selected) => bridge.notify(`mode → ${selected}`), (error) => bridge.notify(`mode switch failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
33647
35196
  return;
33648
35197
  }
33649
- if (line.startsWith("/permission ")) {
35198
+ if (images.length === 0 && line.startsWith("/permission ")) {
33650
35199
  try {
33651
35200
  const selected = setPermissionAction(line.slice(12).trim());
33652
35201
  bridge.notify(`permission → ${selected}`);
@@ -33658,12 +35207,13 @@ async function run(ctx, startup, io) {
33658
35207
  if (session === void 0) {
33659
35208
  pendingInputs.push({
33660
35209
  text: line,
33661
- mode
35210
+ mode,
35211
+ images
33662
35212
  });
33663
35213
  ensureSession();
33664
35214
  return;
33665
35215
  }
33666
- deliverLine(line, mode);
35216
+ deliverLine(line, mode, images);
33667
35217
  };
33668
35218
  /** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
33669
35219
  const dispatch = (text) => {
@@ -33995,6 +35545,47 @@ async function run(ctx, startup, io) {
33995
35545
  label: id.slice(-12)
33996
35546
  });
33997
35547
  };
35548
+ const reviewChanges = (argument) => {
35549
+ loadGitDiff(session?.header.cwd ?? cwd, argument).then(({ title, files }) => {
35550
+ try {
35551
+ setPermissionAction("read-only");
35552
+ } catch (error) {
35553
+ bridge.notify(`review unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
35554
+ return;
35555
+ }
35556
+ send(buildReviewPrompt(files.flatMap((file) => file.lines).join("\n"), title), "followup");
35557
+ bridge.notify("review started under read-only permissions");
35558
+ }, (error) => {
35559
+ bridge.notify(`review failed: ${error instanceof Error ? error.message : String(error)}`, "error");
35560
+ });
35561
+ };
35562
+ const forkSession = (argument) => {
35563
+ if (session === void 0 || active === void 0) {
35564
+ bridge.notify("no session yet - submit a message to start", "warning");
35565
+ return;
35566
+ }
35567
+ try {
35568
+ const text = argument.trim();
35569
+ const atSeq = text === "" ? void 0 : Number(text);
35570
+ if (text !== "" && (!Number.isSafeInteger(atSeq) || (atSeq ?? -1) < 0)) throw new Error("usage: /fork [event-seq]");
35571
+ const seed = selectForkSeed(session.events, atSeq);
35572
+ const id = `session-${randomUUID()}`;
35573
+ requestSwitch({
35574
+ target: {
35575
+ sessionId: id,
35576
+ resume: false,
35577
+ mode: active.mode,
35578
+ cwd: session.header.cwd ?? cwd,
35579
+ seed: seed.events,
35580
+ parentSession: session.id,
35581
+ seedLength: seed.events.length
35582
+ },
35583
+ label: id.slice(-12)
35584
+ });
35585
+ } catch (error) {
35586
+ bridge.notify(`fork failed: ${error instanceof Error ? error.message : String(error)}`, "error");
35587
+ }
35588
+ };
33998
35589
  const switchSession = (row) => {
33999
35590
  if (!row.resumable) {
34000
35591
  bridge.notify("subagent conversations are read-only", "warning");
@@ -34043,6 +35634,7 @@ async function run(ctx, startup, io) {
34043
35634
  loadModelProviders: () => loadProviderSettings(ctx),
34044
35635
  subscribeModelProviders: (listener) => subscribeProviderSettings(ctx, listener),
34045
35636
  saveModelProviderCredential: (target, key) => saveProviderCredential(ctx, target, key),
35637
+ saveModelProviderConfiguration: (target, configuration) => saveProviderConfiguration(ctx, target, configuration),
34046
35638
  unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
34047
35639
  removeModelProvider: (target) => removeProviderSettings(ctx, target),
34048
35640
  loadMentions: (query, signal) => mentions.candidates(query, signal),
@@ -34055,10 +35647,14 @@ async function run(ctx, startup, io) {
34055
35647
  deleteSession,
34056
35648
  exportTranscript,
34057
35649
  renameTitle,
35650
+ copyLastResponse,
35651
+ loadGitDiff: (argument) => loadGitDiff(session?.header.cwd ?? cwd, argument),
35652
+ reviewChanges,
34058
35653
  loadPresets: () => presets.list(),
34059
35654
  switchMode: switchModeAction,
34060
35655
  loadPermissions: () => permissionPresets === void 0 ? Promise.reject(/* @__PURE__ */ new Error("permission presets are not mounted in this composition")) : Promise.resolve(listPermissionRows(permissionPresets)),
34061
35656
  createSession,
35657
+ forkSession,
34062
35658
  loadSessions,
34063
35659
  loadSessionTranscript,
34064
35660
  loadSubagents: () => {
@@ -34070,11 +35666,12 @@ async function run(ctx, startup, io) {
34070
35666
  sort: "newest",
34071
35667
  currentCwd: current.header.cwd ?? cwd,
34072
35668
  query: ""
34073
- }).then((rows) => rows.filter((row) => row.parent === current.id));
35669
+ }).then((rows) => rows.filter((row) => row.parent === current.id && row.subagent));
34074
35670
  },
34075
35671
  switchSession,
34076
35672
  cancelSessionSwitch,
34077
35673
  loadPlugins: () => listPluginRows(ctx),
35674
+ loadJobs: () => listJobs(ctx, active?.agent),
34078
35675
  statusline: statuslineItems,
34079
35676
  saveStatusline,
34080
35677
  saveTheme,
@@ -34090,6 +35687,13 @@ async function run(ctx, startup, io) {
34090
35687
  mountRef.current?.rerender(appElement());
34091
35688
  };
34092
35689
  mountRef.current = io.mount(appElement());
35690
+ if (startup.prompt !== void 0 || (startup.images?.length ?? 0) > 0) saveImagePaths(startup.images ?? [], ctx.get("attachments")).then((images) => send(startup.prompt ?? "", "followup", images), (error) => bridge.notify(`initial prompt failed: ${error instanceof Error ? error.message : String(error)}`, "error"));
35691
+ async function copyLastResponse() {
35692
+ const text = latestAssistantText(store.getView());
35693
+ if (text === void 0) return "nothing to copy yet";
35694
+ await copyText(text);
35695
+ return "copied latest response";
35696
+ }
34093
35697
  if (statuslineWarning !== void 0) setTimeout(() => {
34094
35698
  bridge.notify("statusline config unreadable, using defaults: " + statuslineWarning, "warning");
34095
35699
  }, 50);
@@ -34104,22 +35708,27 @@ async function run(ctx, startup, io) {
34104
35708
  */
34105
35709
  function apply(ctx, config) {
34106
35710
  const theme = config.startup.theme === void 0 ? void 0 : parseThemeName(config.startup.theme);
35711
+ const input = {
35712
+ ...theme === void 0 ? {} : { theme },
35713
+ ...config.startup.prompt === void 0 ? {} : { prompt: config.startup.prompt },
35714
+ ...config.startup.images === void 0 ? {} : { images: config.startup.images }
35715
+ };
34107
35716
  const startup = config.startup.kind === "resume" && config.startup.sessionId !== void 0 ? {
34108
35717
  kind: "resume",
34109
35718
  sessionId: config.startup.sessionId,
34110
- ...theme === void 0 ? {} : { theme }
35719
+ ...input
34111
35720
  } : config.startup.kind === "latest" ? {
34112
35721
  kind: "latest",
34113
- ...theme === void 0 ? {} : { theme }
35722
+ ...input
34114
35723
  } : config.startup.kind === "named" && config.startup.sessionId !== void 0 ? {
34115
35724
  kind: "named",
34116
35725
  sessionId: config.startup.sessionId,
34117
35726
  ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
34118
- ...theme === void 0 ? {} : { theme }
35727
+ ...input
34119
35728
  } : {
34120
35729
  kind: "fresh",
34121
35730
  ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
34122
- ...theme === void 0 ? {} : { theme }
35731
+ ...input
34123
35732
  };
34124
35733
  const exit = ctx.get("appExit");
34125
35734
  if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");