dsh-code 0.9.1 → 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 +29 -13
  2. package/README.md +264 -248
  3. package/bin/deepseek.mjs +100 -6
  4. package/cordis.patch.yml +29 -1
  5. package/lib/index.mjs +2223 -687
  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 +10 -39
  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 -141
  33. package/src/app.ts +4455 -3917
  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 +25 -55
  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
  "·",
@@ -26626,6 +26765,8 @@ const DEEPSEEK_WAVE_BANDS = {
26626
26765
  ]]
26627
26766
  }
26628
26767
  };
26768
+ /** Extra display time applied to every Codex ignition style. */
26769
+ const DEEPSEEK_WAVE_DURATION_EXTENSION_MS = 200;
26629
26770
  /** Original Codex duration used as the animation's sampling timeline. */
26630
26771
  function deepseekWaveBaseDuration(tier, style) {
26631
26772
  const pro = tier === "deepseek" || tier === "unknown";
@@ -26643,7 +26784,7 @@ function deepseekWaveBaseDuration(tier, style) {
26643
26784
  * @returns the duration in milliseconds.
26644
26785
  */
26645
26786
  function deepseekWaveDuration(tier, style = "wave") {
26646
- return deepseekWaveBaseDuration(tier, style) + 200;
26787
+ return deepseekWaveBaseDuration(tier, style) + DEEPSEEK_WAVE_DURATION_EXTENSION_MS;
26647
26788
  }
26648
26789
  /** Map the extended display timeline back onto the original Codex samples. */
26649
26790
  function deepseekWaveSampleElapsedMs(tick, tier, style) {
@@ -26661,22 +26802,6 @@ function deepseekWaveStyleRandom(previous) {
26661
26802
  return candidates[Math.floor(Math.random() * candidates.length)] ?? "wave";
26662
26803
  }
26663
26804
  /**
26664
- * Blank-cell background the wave tint blends toward — fixed approximations
26665
- * of the terminal's default background, mirroring Codex's
26666
- * `user_message_bg_rgb` (which derives a near-black / near-white bubble tint
26667
- * from the terminal background). The Ink layer picks the active theme's one.
26668
- */
26669
- const WAVE_BASE_DARK = [
26670
- 16,
26671
- 18,
26672
- 24
26673
- ];
26674
- const WAVE_BASE_LIGHT = [
26675
- 242,
26676
- 244,
26677
- 248
26678
- ];
26679
- /**
26680
26805
  * Tier for a `provider/model` label: a model id containing `flash` runs the
26681
26806
  * single-band flash tier; everything else (pro/reasoner/chat) runs the
26682
26807
  * dual-band deepseek tier. Mirrors Codex's Max→Ultra mapping.
@@ -26770,14 +26895,22 @@ function blendRgb(fg, bg, alpha) {
26770
26895
  ];
26771
26896
  }
26772
26897
  /**
26773
- * 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
26774
26905
  * `paint_bands` + `Canvas::tint` for all three styles. Bands overlap with a
26775
26906
  * max for Wave/Pulse and a SUM for Aurora (Codex differs by style), the
26776
26907
  * weighted hues mix per column (Wave/Pulse always end on hue 0), the tint
26777
26908
  * blends the mixed hue toward the blank-cell base at the style's alpha cap,
26778
26909
  * and Aurora applies its own fade envelope. Returns `null` when the column
26779
26910
  * should stay transparent, so the row returns to no `backgroundColor` on
26780
- * 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.
26781
26914
  * @param tick - wave frame (0, 1, … at DEEPSEEK_WAVE_TICK_MS).
26782
26915
  * @param column - column index in the content row (0..width-1).
26783
26916
  * @param width - content-row width in columns.
@@ -26785,11 +26918,13 @@ function blendRgb(fg, bg, alpha) {
26785
26918
  * @param style - the ignition style.
26786
26919
  * @param hues - the tier's three hues.
26787
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).
26788
26923
  * @returns the blended RGB background, or null for transparent.
26789
26924
  */
26790
- function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
26925
+ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base, row = 0, rows = 1) {
26791
26926
  const total = deepseekWaveBaseDuration(tier, style) / 1e3;
26792
- const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3;
26927
+ const elapsed = deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3 - (row - (rows - 1) / 2) * total * DEEPSEEK_WAVE_ROW_PHASE;
26793
26928
  const fade = style === "aurora" ? envelope(elapsed, total, .25, .4) : 1;
26794
26929
  const weights = [
26795
26930
  0,
@@ -26828,27 +26963,11 @@ function deepseekWaveColumnBg(tick, column, width, tier, style, hues, base) {
26828
26963
  */
26829
26964
  function deepseekWaveSpark(tick) {
26830
26965
  const elapsed = deepseekWaveSampleElapsedMs(tick, "deepseek", "wave");
26831
- if (elapsed < 900) return null;
26832
- 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);
26833
26968
  return SPARK_GLYPHS[frame] ?? null;
26834
26969
  }
26835
26970
  /**
26836
- * The composer BORDER color at a tick: the frame breathes with the wave —
26837
- * the palette's static dim blends toward the tier accent as the crest is
26838
- * alive and back, so the frame glows up while the wave sweeps and settles to
26839
- * dim on both ends (first==last frame==dim, no hard jump).
26840
- * @param tick - wave frame at DEEPSEEK_WAVE_TICK_MS.
26841
- * @param tier - the wave tier.
26842
- * @param hues - the tier's three hues; the border blends toward hues[0].
26843
- * @param dim - the palette's static dim RGB (the resting border color).
26844
- * @returns the blended border RGB.
26845
- */
26846
- function deepseekWaveBorderColor(tick, tier, style, hues, dim) {
26847
- const total = deepseekWaveBaseDuration(tier, style) / 1e3;
26848
- const glow = envelope(deepseekWaveSampleElapsedMs(tick, tier, style) / 1e3, total, total * .25, total * .4);
26849
- return blendRgb(hues[0], dim, .25 + glow * .6);
26850
- }
26851
- /**
26852
26971
  * Whether the `deepseek` wordmark rides the wave at this tick: it fades in
26853
26972
  * shortly after the first crest launches and out before the wave settles,
26854
26973
  * so the brand name surfaces through the sweep's middle. The Ink layer
@@ -26921,9 +27040,9 @@ function effortAboveHigh(effort) {
26921
27040
  /** Lightweight session-directory projection for the /resume picker. */
26922
27041
  /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
26923
27042
  const CASE_INSENSITIVE_FS = process.platform === "win32" || process.platform === "darwin";
26924
- /** True when the header describes a subagent conversation (durable lineage). */
27043
+ /** True only for delegated subagents; ordinary forks also carry lineage. */
26925
27044
  function isSubagentSession(header) {
26926
- return header.origin === "subagent" || header.parentSession !== void 0;
27045
+ return header.origin === "subagent";
26927
27046
  }
26928
27047
  function comparablePath(value) {
26929
27048
  const resolved = resolve(value);
@@ -27143,10 +27262,18 @@ function cacheHitPercent(usage) {
27143
27262
  const STATUS_ITEM_SEPARATOR = " · ";
27144
27263
  /** The Codex-style mode cycle hint appended to the permission badge. */
27145
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;
27146
27267
  /** Free-tail floor in columns: wide enough for the bare percent readout, so
27147
27268
  * the warning stays visible even at 100%+ occupancy. */
27148
27269
  const CONTEXT_MIN_FREE = 5;
27149
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
+ /**
27150
27277
  * Render context occupancy as ONE stepless bar: a solid DeepSeek-blue fill
27151
27278
  * run, a dim dotted free track, and the usage readout riding the track's
27152
27279
  * right edge (`12.3K/1.0M 25%`, shrinking to the bare percent as the track
@@ -27164,7 +27291,7 @@ function contextBar(usedTokens, contextWindow, width) {
27164
27291
  if (width <= 0 || contextWindow <= 0) return [];
27165
27292
  const used = Math.max(0, usedTokens);
27166
27293
  const percent = Math.round(used / contextWindow * 100);
27167
- const readoutTone = percent >= 90 ? "warn" : "value";
27294
+ const readoutTone = percent >= CONTEXT_WARN_PERCENT ? "warn" : "value";
27168
27295
  const freeShare = Math.round(Math.max(0, contextWindow - used) / contextWindow * width);
27169
27296
  const freeColumns = Math.min(width, Math.max(freeShare, CONTEXT_MIN_FREE));
27170
27297
  const usedColumns = Math.max(0, width - freeColumns);
@@ -27356,7 +27483,7 @@ function joinWidth(parts, separator) {
27356
27483
  return width + separator * (parts.length - 1);
27357
27484
  }
27358
27485
  /** Build every candidate group/span with its drop rank and item id. */
27359
- function buildCandidates(facts, stats, busy, enabled) {
27486
+ function buildCandidates(facts, stats, busy, enabled, contextWidth) {
27360
27487
  const identity = [{
27361
27488
  text: busy ? "● " : "○ ",
27362
27489
  tone: busy ? "live" : "meta"
@@ -27478,7 +27605,7 @@ function buildCandidates(facts, stats, busy, enabled) {
27478
27605
  group: { spans: [{
27479
27606
  text: "context ",
27480
27607
  tone: "label"
27481
- }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, 24)] },
27608
+ }, ...contextBar(stats.lastPromptTokens, stats.contextWindow, contextWidth)] },
27482
27609
  rank: RANK_CONTEXT,
27483
27610
  id: "context"
27484
27611
  });
@@ -27567,7 +27694,8 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27567
27694
  const items = options.items ?? DEFAULT_STATUSLINE_ITEMS;
27568
27695
  const enabled = new Set(items);
27569
27696
  const budget = Math.max(1, Math.floor(columns) - WIDTH_SAFETY);
27570
- 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);
27571
27699
  const groupSeparator = visibleColumns(" | ");
27572
27700
  const itemSeparator = visibleColumns(STATUS_ITEM_SEPARATOR);
27573
27701
  const position = new Map(items.map((id, index) => [id, index]));
@@ -27578,12 +27706,51 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27578
27706
  let hint = badge >= 0 && !busy;
27579
27707
  const leftKept = [...orderedLeft];
27580
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
+ };
27581
27722
  const width = () => {
27582
27723
  const leftWidth = joinWidth(leftKept.map((entry) => spansWidth(entry.group.spans)), groupSeparator);
27583
27724
  const rightWidth = joinWidth(rightKept.map((entry) => visibleColumns(entry.span.text)), itemSeparator) + (hint ? visibleColumns(STATUS_CYCLE_HINT) : 0);
27584
27725
  return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth;
27585
27726
  };
27586
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
+ }
27587
27754
  if (hint) {
27588
27755
  hint = false;
27589
27756
  continue;
@@ -27654,6 +27821,7 @@ function layoutStatusBar(facts, stats, columns, options = {}) {
27654
27821
  }
27655
27822
  //#endregion
27656
27823
  //#region src/render/lines.ts
27824
+ /** Width-safe styled physical rows for bounded terminal panels. */
27657
27825
  /** Construct one segment without leaking mutable objects into cached rows. */
27658
27826
  function lineSegment(text, style = "plain") {
27659
27827
  return {
@@ -27711,6 +27879,59 @@ function styledLines(segments, columns) {
27711
27879
  function textLines(text, columns, style = "plain") {
27712
27880
  return styledLines([lineSegment(text, style)], columns);
27713
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
+ }
27714
27935
  /** Markdown rows re-hardened so a single long word cannot escape the budget. */
27715
27936
  function markdownLines(text, columns) {
27716
27937
  const width = Math.max(1, Math.floor(columns));
@@ -27728,68 +27949,517 @@ function reasoningLines(text, columns) {
27728
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] }));
27729
27950
  }
27730
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
+ */
27731
27958
  function toolDetailLines(detail, columns) {
27732
27959
  switch (detail.kind) {
27733
- case "diff": return detail.diffs.flatMap((diff) => [...styledLines([
27734
- lineSegment(" ── ", "dim"),
27735
- lineSegment(diff.path, "dim"),
27736
- lineSegment(diff.truncated ? " (diff truncated)" : "", "dim")
27737
- ], columns), ...diff.lines.flatMap((line) => styledLines([lineSegment(` ${line.mark}${line.text}`, line.mark === "+" ? "success" : line.mark === "-" ? "error" : "dim")], columns))]);
27738
- 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"))];
27739
- 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")];
27740
- case "web-fetch": return textLines(` ${detail.url} · HTTP ${detail.statusCode}`, columns, "dim");
27741
- 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")];
27742
27965
  default: return detail;
27743
27966
  }
27744
27967
  }
27745
27968
  /**
27746
27969
  * Convert one durable transcript entry to its complete scrollable row model.
27747
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.
27748
27973
  */
27749
- function transcriptEntryLines(entry, columns) {
27974
+ function transcriptEntryLines(entry, columns, showReasoning = true, reasoningToggleHint = true) {
27750
27975
  const width = Math.max(1, Math.floor(columns));
27751
27976
  switch (entry.kind) {
27752
- case "user": return styledLines([lineSegment(entry.notice ? "⤷ " : " ", entry.notice ? "dim" : "brand"), lineSegment(entry.text, entry.notice ? "dim" : "plain")], width);
27753
- 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");
27754
27979
  case "assistant": {
27755
- 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");
27756
27981
  const body = markdownLines(entry.text, Math.max(10, width - 2)).map((line) => ({ segments: [{
27757
27982
  text: " ",
27758
27983
  style: "plain"
27759
27984
  }, ...line.segments] }));
27760
- return [...reasoning, ...body];
27985
+ const interrupted = entry.interrupted === true ? textLines(" ⏹ interrupted", width, "dim") : [];
27986
+ return [
27987
+ ...reasoning,
27988
+ ...body,
27989
+ ...interrupted
27990
+ ];
27761
27991
  }
27762
27992
  case "tool": {
27763
27993
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27764
27994
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
27995
+ const summaryStyle = entry.state === "error" ? "error" : "dim";
27765
27996
  return [
27766
- ...styledLines([
27767
- lineSegment(`${mark} `, markStyle),
27997
+ ...hangingStyledLines([
27998
+ lineSegment(`[${entry.ordinal}] `, "dim"),
27768
27999
  lineSegment(entry.name, "brand"),
27769
28000
  lineSegment(entry.preview === "" ? "" : ` ${entry.preview}`, "dim")
27770
- ], width),
27771
- ...entry.prompt === "" ? [] : textLines(` └ ${entry.prompt}`, width, "dim"),
27772
- ...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),
27773
28004
  ...entry.detail === void 0 ? [] : toolDetailLines(entry.detail, width)
27774
28005
  ];
27775
28006
  }
27776
28007
  case "command": {
27777
28008
  const mark = entry.state === "running" ? "●" : entry.state === "error" ? "⨯" : "⏺";
27778
28009
  const markStyle = entry.state === "running" ? "brand" : entry.state === "error" ? "error" : "success";
27779
- return [...styledLines([
27780
- lineSegment(`${mark} `, markStyle),
27781
- lineSegment(`/${entry.name}`, "brand"),
27782
- lineSegment(entry.args === "" ? "" : ` ${entry.args}`, "dim")
27783
- ], 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)];
27784
28012
  }
27785
28013
  case "turn-marker": return textLines(` ⏹ ${entry.text}`, width, "dim");
27786
28014
  case "compaction": return textLines(entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${entry.error}`, width, "dim");
27787
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");
27788
- 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", " "))];
27789
28017
  case "error": return textLines(entry.text, width, "error");
27790
28018
  default: return entry;
27791
28019
  }
27792
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
+ }
27793
28463
  //#endregion
27794
28464
  //#region src/kernel-panels.ts
27795
28465
  /** Bounded, composer-safe panels for preset, session, and plugin kernel views. */
@@ -27848,9 +28518,15 @@ function ListFrame(props) {
27848
28518
  wrap: "truncate-end"
27849
28519
  }, truncateColumns(singleLineText(props.footer), viewport.contentColumns)));
27850
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
+ */
27851
28526
  function editQuery(query, input, key) {
27852
- if (key.backspace || key.delete) return query.slice(0, -1);
27853
- 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;
27854
28530
  }
27855
28531
  function ModePanel({ current, load, select, close }) {
27856
28532
  const [rows, setRows] = (0, import_react.useState)([]);
@@ -27975,6 +28651,57 @@ function PluginPanel({ load, close, initialQuery = "" }) {
27975
28651
  footer: "↑↓ inspect · enter details · r refresh · esc close"
27976
28652
  });
27977
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
+ }
27978
28705
  function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete, deleteConfirmId, reloadToken = 0, deleteMode = false, close }) {
27979
28706
  const [options, setOptions] = (0, import_react.useState)({
27980
28707
  sessions: "roots",
@@ -28122,11 +28849,42 @@ function ResumePanel({ currentCwd, load, readTranscript, select, requestDelete,
28122
28849
  footer: "tab/←→ filters · ↑↓/pg navigate · ctrl+f search · e details · t transcript · d delete · enter resume"
28123
28850
  });
28124
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
+ }
28125
28883
  function DocumentPanel({ title, text, error, close }) {
28126
28884
  const stdout = useStdout().stdout;
28127
28885
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
28128
28886
  const [scroll, setScroll] = (0, import_react.useState)(0);
28129
- 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]);
28130
28888
  useInput((input, key) => {
28131
28889
  if (key.escape || input === "q" || input === "t") return close();
28132
28890
  if (key.upArrow) return setScroll((value) => Math.max(0, value - 1));
@@ -28137,7 +28895,7 @@ function DocumentPanel({ title, text, error, close }) {
28137
28895
  if (input === "G") return setScroll(Math.max(0, lines.length - viewport.bodyRows));
28138
28896
  });
28139
28897
  if (viewport.compact) return (0, import_react.createElement)(Text, { wrap: "truncate-end" }, truncateColumns("transcript · esc close", viewport.contentColumns));
28140
- 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);
28141
28899
  return (0, import_react.createElement)(Box, {
28142
28900
  width: viewport.outerColumns,
28143
28901
  borderStyle: "round",
@@ -28147,10 +28905,7 @@ function DocumentPanel({ title, text, error, close }) {
28147
28905
  }, (0, import_react.createElement)(Text, {
28148
28906
  color: inkColor(getPalette().brandBright),
28149
28907
  wrap: "truncate-end"
28150
- }, truncateColumns(singleLineText(title), viewport.contentColumns)), ...body.map((line, index) => (0, import_react.createElement)(Text, {
28151
- key: `${scroll}-${index}`,
28152
- wrap: "truncate-end"
28153
- }, 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, {
28154
28909
  dimColor: true,
28155
28910
  wrap: "truncate-end"
28156
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)));
@@ -28177,13 +28932,16 @@ function HistoryPanel({ entries, fill, close }) {
28177
28932
  if (input === "g") return setCursor(0);
28178
28933
  if (input === "G") return setCursor(matches.length - 1);
28179
28934
  if (key.backspace) {
28180
- setQuery((current) => current.slice(0, -1));
28935
+ setQuery((current) => deleteLastGrapheme(current));
28181
28936
  setCursor(0);
28182
28937
  return;
28183
28938
  }
28184
28939
  if (input !== "" && !key.ctrl && !key.meta && !key.shift) {
28185
- setQuery((current) => (current + input).slice(0, 120));
28186
- setCursor(0);
28940
+ const text = stripPasteMarkers(input);
28941
+ if (text !== "") {
28942
+ setQuery((current) => (current + text).slice(0, 120));
28943
+ setCursor(0);
28944
+ }
28187
28945
  }
28188
28946
  });
28189
28947
  const stdout = useStdout().stdout;
@@ -28614,6 +29372,15 @@ function recallEntries(persistent, local) {
28614
29372
  const localSet = new Set(local);
28615
29373
  return [...persistent.filter((entry) => !localSet.has(entry)), ...local].reverse();
28616
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
+ }
28617
29384
  /**
28618
29385
  * Move one entry older (Up, toward index +1 in the newest-first space). The
28619
29386
  * first Up saves the current draft so Down past the newest can restore it
@@ -28697,8 +29464,32 @@ function recallNewer(state) {
28697
29464
  */
28698
29465
  /** Match Codex's settled-resize window before rebuilding terminal scrollback. */
28699
29466
  const RESIZE_REFLOW_DELAY_MS = 75;
28700
- /** Reset region/style, clear the visible screen and scrollback, then home. */
28701
- 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";
28702
29493
  /** Pad text with spaces to a visible-column target (menu name column). */
28703
29494
  function padColumns(text, width) {
28704
29495
  const clipped = truncateColumns(singleLineText(text), width);
@@ -28729,11 +29520,6 @@ function useStableInput(handler, active) {
28729
29520
  }, []);
28730
29521
  useInput(stableHandler, { isActive: active });
28731
29522
  }
28732
- /** Single-cell stepped pulse: the web's 125ms flat-hold brightness steps over 1s. */
28733
- function Pulse() {
28734
- const tick = useFrames(125);
28735
- return (0, import_react.createElement)(Text, { color: inkColor(getPalette().brandBright) }, pulseFrame(tick));
28736
- }
28737
29523
  /**
28738
29524
  * The web StateDot "ongoing" chase in terminal form: three cells of the 3×3
28739
29525
  * ring trail clockwise around the eight outer positions (8 frames × 125ms =
@@ -28754,13 +29540,6 @@ function CursorBlock({ char }) {
28754
29540
  const tick = useFrames(530);
28755
29541
  return (0, import_react.createElement)(Text, { inverse: caretVisible(tick) || void 0 }, char);
28756
29542
  }
28757
- /** Web TurnStatus elapsed format: `45s` under a minute, `2m03s` beyond. */
28758
- function runClock(ms) {
28759
- const total = Math.max(0, Math.floor(ms / 1e3));
28760
- const minutes = Math.floor(total / 60);
28761
- const seconds = total % 60;
28762
- return minutes > 0 ? `${minutes}m${String(seconds).padStart(2, "0")}s` : `${seconds}s`;
28763
- }
28764
29543
  /**
28765
29544
  * The busy line, web TurnStatus contract: the StateDot chase leads the plain
28766
29545
  * `Deep diving...` label, with the elapsed clock appended only once the turn
@@ -28784,7 +29563,7 @@ function StreamTail({ text, dim, maxRows, prefix = "", continuationPrefix = pref
28784
29563
  const columns = useStdout().stdout?.columns ?? 80;
28785
29564
  const safeRows = Math.max(1, maxRows);
28786
29565
  const prefixColumns = Math.max(visibleColumns(prefix), visibleColumns(continuationPrefix));
28787
- const contentColumns = Math.max(10, columns - 3 - prefixColumns);
29566
+ const contentColumns = Math.max(10, columns - 1 - prefixColumns);
28788
29567
  const initial = displayTail(text, contentColumns, safeRows);
28789
29568
  const tail = initial.truncated && safeRows > 1 ? displayTail(text, contentColumns, safeRows - 1) : initial;
28790
29569
  const rows = tail.text.split("\n");
@@ -28906,138 +29685,80 @@ function StyledRows({ lines }) {
28906
29685
  ...lineStyleProps(segment.style)
28907
29686
  }, segment.text)))));
28908
29687
  }
28909
- /** Codex-style panel rhythm that still participates in the row budget. */
28910
- function PanelGap({ visible }) {
28911
- return visible ? (0, import_react.createElement)(Text, null, " ") : void 0;
28912
- }
28913
- /** One settled markdown document rendered as styled lines at the terminal width. */
28914
- function MarkdownBody({ text, indent = 0 }) {
28915
- const columns = useStdout().stdout?.columns ?? 80;
28916
- const lines = (0, import_react.useMemo)(() => renderMarkdown(displayText(text), Math.max(20, columns - 2 - indent)), [
28917
- text,
28918
- columns,
28919
- indent
28920
- ]);
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));
28921
29716
  return (0, import_react.createElement)(Box, {
28922
29717
  flexDirection: "column",
28923
- paddingLeft: indent
28924
- }, ...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, {
28925
- key: at,
28926
- ...segmentProps(segment.style)
28927
- }, segment.text)))));
28928
- }
28929
- /** Expanded reasoning with the same two-column content edge as the reply. */
28930
- function ReasoningBody({ text }) {
28931
- const columns = useStdout().stdout?.columns ?? 80;
28932
- const lines = (0, import_react.useMemo)(() => reasoningLines(text, Math.max(10, columns - 2)), [text, columns]);
28933
- return (0, import_react.createElement)(StyledRows, { lines });
28934
- }
28935
- /**
28936
- * One expanded tool-card body for the verbose transcript (Ctrl+O): the
28937
- * presentation contract's structured cards — inline diffs, read windows,
28938
- * web sources — rendered as plain terminal rows, degradation-safe against
28939
- * replayed metadata.
28940
- */
28941
- function ToolDetailBody({ detail }) {
28942
- switch (detail.kind) {
28943
- case "diff": return (0, import_react.createElement)(Box, { flexDirection: "column" }, ...detail.diffs.map((diff, index) => (0, import_react.createElement)(Box, {
28944
- key: index,
28945
- flexDirection: "column"
28946
- }, (0, import_react.createElement)(Text, {
28947
- dimColor: true,
28948
- wrap: "truncate-end"
28949
- }, ` ── ${displayText(diff.path)}${diff.truncated ? " (diff truncated)" : ""}`), ...diff.lines.map((line, at) => (0, import_react.createElement)(Text, {
28950
- key: at,
28951
- color: line.mark === "+" ? inkColor(getPalette().success) : line.mark === "-" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
28952
- wrap: "truncate-end"
28953
- }, ` ${line.mark}${displayText(line.text)}`)))));
28954
- case "read": return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(Text, {
28955
- dimColor: true,
28956
- wrap: "truncate-end"
28957
- }, ` ── ${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, {
28958
- key: at,
28959
- dimColor: true,
28960
- wrap: "truncate-end"
28961
- }, ` ${String(line.number).padStart(5, " ")} | ${displayText(line.text)}`)));
28962
- case "web-search": return (0, import_react.createElement)(Box, { flexDirection: "column" }, ...detail.sources.map((source, at) => (0, import_react.createElement)(Text, {
28963
- key: at,
28964
- wrap: "truncate-end"
28965
- }, 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)" : ""}`)));
28966
- case "web-fetch": return (0, import_react.createElement)(Text, {
28967
- dimColor: true,
28968
- wrap: "truncate-end"
28969
- }, dim(` ${displayText(detail.url)} · HTTP ${detail.statusCode}`));
28970
- 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, {
28971
- key: at,
28972
- dimColor: true,
28973
- wrap: "truncate-end"
28974
- }, ` ${line}`)), (0, import_react.createElement)(Text, { dimColor: true }, detail.truncated ? " … (output truncated)" : " (end of output)"));
28975
- default: return assertNever(detail, "tool detail kind");
28976
- }
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)));
28977
29729
  }
28978
- /** One settled transcript row. */
28979
- function EntryLine({ entry, showReasoning, verbose }) {
28980
- switch (entry.kind) {
28981
- 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));
28982
- 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, {
28983
- text: entry.text,
28984
- indent: 2
28985
- }));
28986
- case "tool": {
28987
- 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) }, "⏺");
28988
- 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, {
28989
- color: entry.state === "error" ? inkColor(getPalette().error) : inkColor(getPalette().dim),
28990
- wrap: verbose ? "truncate-end" : void 0
28991
- }, ` ⎿ ${displayText(entry.summary)}`), verbose && entry.detail !== void 0 ? (0, import_react.createElement)(ToolDetailBody, { detail: entry.detail }) : void 0);
28992
- }
28993
- case "command": {
28994
- 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) }, "⏺");
28995
- 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, {
28996
- color: inkColor(getPalette().dim),
28997
- wrap: verbose ? "truncate-end" : void 0
28998
- }, ` ⎿ ${displayText(entry.summary)}`));
28999
- }
29000
- case "turn-marker": return (0, import_react.createElement)(Text, {
29001
- dimColor: true,
29002
- wrap: verbose ? "truncate-end" : void 0
29003
- }, ` ⏹ ${displayText(entry.text)}`);
29004
- case "compaction": return (0, import_react.createElement)(Text, {
29005
- dimColor: true,
29006
- wrap: verbose ? "truncate-end" : void 0
29007
- }, entry.ok ? ` ⧉ compacted ~${formatTokens(entry.tokens)} tokens` : ` ⧉ compaction failed: ${displayText(entry.error)}`);
29008
- case "retry": return (0, import_react.createElement)(Text, {
29009
- color: entry.state === "running" ? inkColor(getPalette().warn) : inkColor(getPalette().dim),
29010
- wrap: verbose ? "truncate-end" : void 0
29011
- }, ` ↻ retry ${entry.attempt}/${entry.max} · ${displayText(entry.code)} · ${Math.round(entry.delayMs / 100) / 10}s`);
29012
- case "files": {
29013
- const shown = entry.paths.slice(0, 3).map((path) => displayText(path)).join(" · ");
29014
- const more = entry.paths.length > 3 ? ` (+${entry.paths.length - 3} more)` : "";
29015
- return (0, import_react.createElement)(Text, {
29016
- dimColor: true,
29017
- wrap: verbose ? "truncate-end" : void 0
29018
- }, ` ⎄ ${shown}${more}`);
29019
- }
29020
- case "pending": return (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, brand("❯ "), displayText(entry.text));
29021
- case "error": return (0, import_react.createElement)(Text, { wrap: verbose ? "truncate-end" : void 0 }, error(displayText(entry.text)));
29022
- default: return assertNever(entry, "transcript entry kind");
29023
- }
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;
29024
29733
  }
29025
29734
  /**
29026
- * The whale header with a compact three-line copy lockup. The title, bilingual
29027
- * slogan, and key hint stay centered inside the existing eight content rows,
29028
- * preserving the Static header's ten physical rows. Short or narrow terminals
29029
- * 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.
29030
29741
  */
29031
29742
  function Header({ resumed }) {
29032
29743
  const stdout = useStdout().stdout;
29033
29744
  const rows = stdout?.rows ?? 40;
29034
29745
  const columns = stdout?.columns ?? 80;
29746
+ const kernelLine = (() => {
29747
+ const version = dshKernelVersion();
29748
+ return version === void 0 ? void 0 : `dsh-v${version}`;
29749
+ })();
29035
29750
  const title = `DeepSeek Harness · v${DSH_CODE_VERSION}`;
29036
29751
  const slogan = "Into the Unknown 探索未至之境";
29037
29752
  const hint = resumed ? "resumed · /help · Esc interrupt" : "/help · Esc interrupt · Ctrl+C quit";
29038
- 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);
29039
29760
  const compact = `${title} · ${hint}`;
29040
- 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, {
29041
29762
  width: Math.max(1, columns - 1),
29042
29763
  borderStyle: "round",
29043
29764
  borderColor: inkColor(getPalette().brand),
@@ -29052,7 +29773,7 @@ function Header({ resumed }) {
29052
29773
  gap: 2,
29053
29774
  borderStyle: "round",
29054
29775
  borderColor: inkColor(getPalette().brand),
29055
- paddingX: 1,
29776
+ paddingX: 2,
29056
29777
  alignSelf: "flex-start"
29057
29778
  }, (0, import_react.createElement)(Box, {
29058
29779
  flexDirection: "column",
@@ -29065,7 +29786,10 @@ function Header({ resumed }) {
29065
29786
  flexDirection: "column",
29066
29787
  width: copyColumns,
29067
29788
  justifyContent: "center"
29068
- }, (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, {
29069
29793
  color: inkColor(getPalette().brandBright),
29070
29794
  bold: true,
29071
29795
  wrap: "truncate-end"
@@ -29109,8 +29833,69 @@ function TodoPanel({ todos }) {
29109
29833
  color: inkColor(getPalette().brand),
29110
29834
  bold: true,
29111
29835
  wrap: "truncate-end"
29112
- }, `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))));
29113
29897
  }
29898
+ const MemoTodoListPanel = (0, import_react.memo)(TodoListPanel);
29114
29899
  /**
29115
29900
  * Ink props for one status tone: the Codex status-line accent mapping over
29116
29901
  * the DeepSeek palette, all blue by design — the status bar speaks only in
@@ -29193,7 +29978,7 @@ function statusToneProps(tone) {
29193
29978
  * truncation degrades groups, it never wraps a row.
29194
29979
  *
29195
29980
  * The DeepSeek easter egg: when the model label *switches* to an official
29196
- * 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
29197
29982
  * effort-ignition "Wave" — a blue crest sweeping the content row column by
29198
29983
  * column, with the `· ✦ ✧` sparkles on the deepseek tier — and the prompt
29199
29984
  * marker keeps the tier accent afterwards. The border stays a constant
@@ -29219,10 +30004,28 @@ function deepseekWaveHues(tier) {
29219
30004
  ];
29220
30005
  }
29221
30006
  function StatusLine({ facts, stats, busy, columns, items }) {
29222
- 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), {
29223
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,
30025
+ busy,
30026
+ columns,
29224
30027
  items
29225
- });
30028
+ ]);
29226
30029
  const renderRow = (row, key, indent = 0) => {
29227
30030
  const leftParts = [];
29228
30031
  row.left.forEach((group, groupIndex) => {
@@ -29591,10 +30394,13 @@ function QuestionBar({ store, snapshot, locked }) {
29591
30394
  returnToOptions();
29592
30395
  return;
29593
30396
  }
29594
- setCustom((current) => current.slice(0, -1));
30397
+ setCustom((current) => deleteLastGrapheme(current));
29595
30398
  return;
29596
30399
  }
29597
- 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
+ }
29598
30404
  return;
29599
30405
  }
29600
30406
  if (key.upArrow) {
@@ -29761,7 +30567,7 @@ function providerStateLabel(row) {
29761
30567
  return `${route} · ${row.configured ? "provider auth" : "not configured"}`;
29762
30568
  }
29763
30569
  /** The provider-management stage reached from /model with `a`. */
29764
- function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRetry, onBack }) {
30570
+ function ProviderPanel({ directory, error, onCredential, onConfigure, onUnset, onRemove, onRetry, onBack }) {
29765
30571
  const stdout = useStdout().stdout;
29766
30572
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29767
30573
  const rows = directory?.rows ?? [];
@@ -29807,6 +30613,11 @@ function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRe
29807
30613
  }
29808
30614
  const target = rows[cursor];
29809
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
+ }
29810
30621
  if (input === "d") {
29811
30622
  const facts = target.credential;
29812
30623
  if (facts?.kind !== "facts" || !facts.configured) setActionError("this provider has no configured API key to remove");
@@ -29878,7 +30689,143 @@ function ProviderPanel({ directory, error, onCredential, onUnset, onRemove, onRe
29878
30689
  }), (0, import_react.createElement)(PanelGap, { visible: viewport.gapRows > 0 }), (0, import_react.createElement)(Text, {
29879
30690
  color: inkColor(getPalette().dim),
29880
30691
  wrap: "truncate-end"
29881
- }, 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)));
29882
30829
  }
29883
30830
  /** Write-only masked API-key editor; the secret lives only in this mounted component. */
29884
30831
  function ProviderCredentialPanel({ target, save, done, back }) {
@@ -29921,7 +30868,7 @@ function ProviderCredentialPanel({ target, save, done, back }) {
29921
30868
  return;
29922
30869
  }
29923
30870
  if (key.ctrl || key.meta || input.length === 0) return;
29924
- const next = draft + input;
30871
+ const next = draft + stripPasteMarkers(input);
29925
30872
  if (next.length > 4096) {
29926
30873
  setError("API key input is too long");
29927
30874
  return;
@@ -30060,7 +31007,7 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
30060
31007
  key: "key-mentions",
30061
31008
  dimColor: true,
30062
31009
  wrap: "truncate-end"
30063
- }, " tab also completes bare workspace paths · @ mentions files and sessions"),
31010
+ }, " @ mentions workspace files and sessions"),
30064
31011
  (0, import_react.createElement)(Text, {
30065
31012
  key: "key-inspector",
30066
31013
  dimColor: true,
@@ -30098,17 +31045,23 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
30098
31045
  (0, import_react.createElement)(Box, { key: "local-mode" }, row("/mode", "inspect or select the agent preset (/mode [preset])")),
30099
31046
  (0, import_react.createElement)(Box, { key: "local-permission" }, row("/permission", "inspect or select the permission preset (/permission [preset])")),
30100
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])")),
30101
31049
  (0, import_react.createElement)(Box, { key: "local-resume" }, row("/resume", "browse or switch root sessions (/resume [id|prefix])")),
30102
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")),
30103
31052
  (0, import_react.createElement)(Box, { key: "local-statusline" }, row("/statusline", "customize the status line items")),
30104
31053
  (0, import_react.createElement)(Box, { key: "local-theme" }, row("/theme", "switch the color theme")),
30105
31054
  (0, import_react.createElement)(Box, { key: "local-history" }, row("/history", "search and recall past prompts")),
30106
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")),
30107
31057
  (0, import_react.createElement)(Box, { key: "local-subagent" }, row("/subagent", "choose the model delegated subagents run on")),
30108
31058
  (0, import_react.createElement)(Box, { key: "local-delete" }, row("/delete", "delete a session and its subagent threads")),
30109
31059
  (0, import_react.createElement)(Box, { key: "local-clear" }, row("/clear", "clear the screen")),
30110
31060
  (0, import_react.createElement)(Box, { key: "local-export" }, row("/export", "export the transcript to markdown (/export [path])")),
30111
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")),
30112
31065
  (0, import_react.createElement)(Box, { key: "local-quit" }, row("/quit", "exit")),
30113
31066
  ...descriptors.map((descriptor) => (0, import_react.createElement)(Text, {
30114
31067
  key: `command-${descriptor.name}`,
@@ -30171,19 +31124,47 @@ function HelpPanel({ descriptors, skills, commandError, skillError, onClose }) {
30171
31124
  function verboseLine(text, columns) {
30172
31125
  return truncateColumns(displayText(text).replace(/\n/gu, " ↵ ").replace(/\t/gu, " "), Math.max(1, columns));
30173
31126
  }
30174
- /** 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
+ */
30175
31154
  function editorWindow(value, cursor, columns) {
30176
31155
  const width = Math.max(1, columns);
30177
31156
  const normalize = (text) => displayText(text).replace(/\n/gu, "↵").replace(/\t/gu, " ");
30178
- const caretSource = value.slice(cursor, cursor + 1);
30179
- 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);
30180
31161
  const remaining = Math.max(0, width - visibleColumns(caret));
30181
- 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)));
30182
31163
  const beforeBudget = Math.max(0, remaining - afterBudget);
30183
31164
  return {
30184
- 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,
30185
31166
  caret,
30186
- after: afterBudget === 0 ? "" : truncateColumns(normalize(value.slice(cursor + 1)), afterBudget)
31167
+ after: afterBudget === 0 ? "" : truncateColumns(normalize(rest), afterBudget)
30187
31168
  };
30188
31169
  }
30189
31170
  /** The empty-composer placeholder text (shared by the static and wave paths). */
@@ -30382,6 +31363,11 @@ function completionCandidates(value, descriptors, skills) {
30382
31363
  description: "start a fresh session",
30383
31364
  origin: "command"
30384
31365
  },
31366
+ {
31367
+ label: "/fork",
31368
+ description: "fork at a completed turn",
31369
+ origin: "command"
31370
+ },
30385
31371
  {
30386
31372
  label: "/resume",
30387
31373
  description: "browse or switch sessions",
@@ -30392,6 +31378,11 @@ function completionCandidates(value, descriptors, skills) {
30392
31378
  description: "inspect the plugin composition",
30393
31379
  origin: "command"
30394
31380
  },
31381
+ {
31382
+ label: "/jobs",
31383
+ description: "inspect background jobs",
31384
+ origin: "command"
31385
+ },
30395
31386
  {
30396
31387
  label: "/statusline",
30397
31388
  description: "customize the status line",
@@ -30412,6 +31403,11 @@ function completionCandidates(value, descriptors, skills) {
30412
31403
  description: "inspect subagent sessions of this conversation",
30413
31404
  origin: "command"
30414
31405
  },
31406
+ {
31407
+ label: "/todos",
31408
+ description: "inspect the full todo list",
31409
+ origin: "command"
31410
+ },
30415
31411
  {
30416
31412
  label: "/subagent",
30417
31413
  description: "choose the model delegated subagents run on",
@@ -30437,6 +31433,21 @@ function completionCandidates(value, descriptors, skills) {
30437
31433
  description: "rename this session",
30438
31434
  origin: "command"
30439
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
+ },
30440
31451
  {
30441
31452
  label: "/quit",
30442
31453
  description: "exit",
@@ -30472,7 +31483,7 @@ function completionCandidates(value, descriptors, skills) {
30472
31483
  }
30473
31484
  /**
30474
31485
  * The completion menu, rendered inside the composer's subtree directly above
30475
- * 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
30476
31487
  * it grows the stack downward: the composer stays the last element on screen
30477
31488
  * and everything above (the flushed static transcript, the status line) never
30478
31489
  * moves. Props-only (no lifted state): the menu is a pure view of the input
@@ -30485,7 +31496,7 @@ function CompletionMenu({ active, mention, index, rows }) {
30485
31496
  const terminalRows = stdout?.rows ?? 30;
30486
31497
  if (!active) return void 0;
30487
31498
  const contentColumns = Math.max(1, columns - 4);
30488
- 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);
30489
31500
  const descBudget = Math.max(0, contentColumns - nameWidth - 2);
30490
31501
  const showFooter = terminalRows >= 12;
30491
31502
  const verticalPadding = terminalRows >= 14 ? 1 : 0;
@@ -30523,26 +31534,32 @@ function CompletionMenu({ active, mention, index, rows }) {
30523
31534
  * While a modal (approval / question / model panel) owns the keys, the
30524
31535
  * box passes every key through untouched.
30525
31536
  */
30526
- 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 }) {
30527
31538
  const columns = useStdout().stdout?.columns ?? 80;
31539
+ const stdin = useStdin().stdin;
30528
31540
  const [value, setValue] = (0, import_react.useState)("");
30529
31541
  const [cursor, setCursor] = (0, import_react.useState)(0);
30530
- const recall = (0, import_react.useRef)({
30531
- entries: [],
30532
- index: null,
30533
- savedDraft: "",
30534
- lastRecalled: null
30535
- });
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([], ""));
30536
31551
  (0, import_react.useEffect)(() => {
30537
31552
  if (historyFill === void 0) return;
30538
- setValue(historyFill.text);
30539
- setCursor(historyFill.text.length);
31553
+ const safe = sanitizeDraftText(historyFill.text);
31554
+ setValue(safe);
31555
+ setCursor(safe.length);
31556
+ preferredColumnRef.current = null;
30540
31557
  setDismissedMenuValue(void 0);
30541
31558
  recall.current = {
30542
31559
  entries: recallSpace,
30543
31560
  index: historyFill.index,
30544
- savedDraft: historyFill.text,
30545
- lastRecalled: historyFill.text
31561
+ savedDraft: safe,
31562
+ lastRecalled: safe
30546
31563
  };
30547
31564
  historyConsumed();
30548
31565
  }, [
@@ -30550,6 +31567,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30550
31567
  recallSpace,
30551
31568
  historyConsumed
30552
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]);
30553
31584
  if (recall.current.entries !== recallSpace) {
30554
31585
  const index = recall.current.index === null || recall.current.index < recallSpace.length ? recall.current.index : null;
30555
31586
  recall.current = {
@@ -30571,27 +31602,6 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30571
31602
  };
30572
31603
  const mentionActive = mentionToken !== void 0;
30573
31604
  const [mentionRows, setMentionRows] = (0, import_react.useState)([]);
30574
- const bareTokenMatch = /([^\s]+)$/u.exec(lastLine);
30575
- const bareToken = bareTokenMatch === null ? "" : bareTokenMatch[1] ?? "";
30576
- const pathActive = !mentionActive && !bareToken.startsWith("/") && (bareToken.includes("/") || bareToken === "." || bareToken === "..");
30577
- const pathTokenStart = beforeCursor.length - bareToken.length;
30578
- const [pathRows, setPathRows] = (0, import_react.useState)([]);
30579
- (0, import_react.useEffect)(() => {
30580
- if (!active || !pathActive) {
30581
- setPathRows([]);
30582
- return;
30583
- }
30584
- const controller = new AbortController();
30585
- setPathRows([]);
30586
- loadMentions(bareToken, controller.signal).then((rows) => setPathRows(rows.filter((row) => row.kind !== "session")), () => {});
30587
- return () => {
30588
- controller.abort();
30589
- };
30590
- }, [
30591
- active,
30592
- pathActive,
30593
- bareToken
30594
- ]);
30595
31605
  (0, import_react.useEffect)(() => {
30596
31606
  if (!active || !mentionActive) {
30597
31607
  setMentionRows([]);
@@ -30608,15 +31618,11 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30608
31618
  mentionActive,
30609
31619
  mentionToken?.query
30610
31620
  ]);
30611
- const menuActive = (slashActive || mentionActive || pathActive) && dismissedMenuValue !== value;
31621
+ const menuActive = (slashActive || mentionActive) && dismissedMenuValue !== value;
30612
31622
  const menuRows = mentionActive ? mentionRows.map((row) => ({
30613
31623
  label: row.label.startsWith("@") ? row.label : `@${row.label}${row.kind === "directory" ? "/" : ""}`,
30614
31624
  description: row.description,
30615
31625
  origin: "mention"
30616
- })) : pathActive ? pathRows.map((row) => ({
30617
- label: row.label,
30618
- description: row.description,
30619
- origin: "path"
30620
31626
  })) : candidates;
30621
31627
  /** Accept the highlighted completion-menu candidate into the draft. */
30622
31628
  const acceptMenuCandidate = () => {
@@ -30627,13 +31633,6 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30627
31633
  setValue(value.slice(0, mentionToken.start) + insertion + value.slice(cursor));
30628
31634
  setCursor(mentionToken.start + insertion.length);
30629
31635
  }
30630
- } else if (pathActive) {
30631
- const row = pathRows[completionIndex % Math.max(1, pathRows.length)];
30632
- if (row !== void 0) {
30633
- const insertion = row.kind === "directory" ? `${row.label}/` : row.label;
30634
- setValue(value.slice(0, pathTokenStart) + insertion + value.slice(cursor));
30635
- setCursor(pathTokenStart + insertion.length);
30636
- }
30637
31636
  } else {
30638
31637
  const candidate = candidates[completionIndex % candidates.length];
30639
31638
  if (candidate !== void 0) {
@@ -30644,6 +31643,21 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30644
31643
  setCompletionIndex(0);
30645
31644
  setDismissedMenuValue(void 0);
30646
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
+ };
30647
31661
  useInput((input, key) => {
30648
31662
  if (!active) return;
30649
31663
  if (deleteConfirm !== void 0) {
@@ -30679,6 +31693,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30679
31693
  return;
30680
31694
  }
30681
31695
  if (key.ctrl && input === "d") {
31696
+ if (value !== "") {
31697
+ applyEdit(deleteForward(value, cursor));
31698
+ return;
31699
+ }
30682
31700
  if (busy) notify("cancel the running turn before exiting (Esc or Ctrl+C)", "warning");
30683
31701
  else quit();
30684
31702
  return;
@@ -30699,15 +31717,20 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30699
31717
  cancelQueued(queued[queued.length - 1].messageId);
30700
31718
  return;
30701
31719
  }
30702
- if (key.return) {
30703
- 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") {
30704
31727
  setValue(value.slice(0, cursor) + "\n" + value.slice(cursor));
30705
31728
  setCursor(cursor + 1);
30706
31729
  setDismissedMenuValue(void 0);
30707
31730
  return;
30708
31731
  }
30709
31732
  if (menuActive) {
30710
- if (!(!mentionActive && !pathActive && candidates.some((candidate) => candidate.label === value))) {
31733
+ if (!(!mentionActive && candidates.some((candidate) => candidate.label === value))) {
30711
31734
  acceptMenuCandidate();
30712
31735
  return;
30713
31736
  }
@@ -30723,12 +31746,7 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30723
31746
  recordLocal(text);
30724
31747
  recordHistory(text);
30725
31748
  }
30726
- recall.current = {
30727
- entries: recallSpace,
30728
- index: null,
30729
- savedDraft: "",
30730
- lastRecalled: null
30731
- };
31749
+ recall.current = beginRecall(recallSpace, "");
30732
31750
  if (text === "/quit") {
30733
31751
  quit();
30734
31752
  return;
@@ -30752,6 +31770,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30752
31770
  notify(outcome, outcome.startsWith("rename failed:") ? "error" : outcome.startsWith("usage:") || outcome.includes("unavailable") ? "warning" : "info");
30753
31771
  return;
30754
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
+ }
30755
31785
  if (text === "/model" || text.startsWith("/model ")) {
30756
31786
  openModel();
30757
31787
  return;
@@ -30786,10 +31816,18 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30786
31816
  createSession(text.slice(4).trim() || void 0);
30787
31817
  return;
30788
31818
  }
31819
+ if (text === "/fork" || text.startsWith("/fork ")) {
31820
+ forkSession(text.slice(5));
31821
+ return;
31822
+ }
30789
31823
  if (text === "/plugin" || text.startsWith("/plugin ")) {
30790
31824
  openPlugin(text.slice(7).trim());
30791
31825
  return;
30792
31826
  }
31827
+ if (text === "/jobs" || text.startsWith("/jobs ")) {
31828
+ openJobs();
31829
+ return;
31830
+ }
30793
31831
  if (text === "/statusline") {
30794
31832
  openStatusline();
30795
31833
  return;
@@ -30806,6 +31844,10 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30806
31844
  openAgents();
30807
31845
  return;
30808
31846
  }
31847
+ if (text === "/todos") {
31848
+ openTodos();
31849
+ return;
31850
+ }
30809
31851
  if (text === "/subagent") {
30810
31852
  openSubagent();
30811
31853
  return;
@@ -30829,25 +31871,58 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30829
31871
  setCompletionIndex((index) => (index + 1) % menuRows.length);
30830
31872
  return;
30831
31873
  }
30832
- if (key.upArrow) {
30833
- if (recall.current.entries.length === 0) return;
30834
- const step = recallOlder(recall.current, value);
30835
- recall.current = step.state;
30836
- if (step.entry !== void 0) {
30837
- setValue(step.entry);
30838
- setCursor(step.entry.length);
30839
- 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;
30840
31903
  }
30841
31904
  return;
30842
31905
  }
30843
- if (key.downArrow) {
30844
- if (recall.current.entries.length === 0) return;
30845
- const step = recallNewer(recall.current);
30846
- recall.current = step.state;
30847
- if (step.entry !== void 0) {
30848
- setValue(step.entry);
30849
- setCursor(step.entry.length);
30850
- 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;
30851
31926
  }
30852
31927
  return;
30853
31928
  }
@@ -30855,51 +31930,88 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30855
31930
  acceptMenuCandidate();
30856
31931
  return;
30857
31932
  }
30858
- if (key.backspace || key.delete) {
30859
- if (cursor > 0) {
30860
- setValue(value.slice(0, cursor - 1) + value.slice(cursor));
30861
- setCursor(cursor - 1);
30862
- setCompletionIndex(0);
30863
- setDismissedMenuValue(void 0);
30864
- }
30865
- return;
30866
- }
30867
31933
  if (key.leftArrow) {
30868
- setCursor(Math.max(0, cursor - 1));
31934
+ moveCursorTo(key.meta || key.ctrl ? moveWordLeft(value, cursor) : moveCursorBy(value, cursor, -1));
30869
31935
  return;
30870
31936
  }
30871
31937
  if (key.rightArrow) {
30872
- setCursor(Math.min(value.length, cursor + 1));
31938
+ moveCursorTo(key.meta || key.ctrl ? moveWordRight(value, cursor) : moveCursorBy(value, cursor, 1));
30873
31939
  return;
30874
31940
  }
30875
- if (key.ctrl && input === "u") {
30876
- setValue("");
30877
- setCursor(0);
30878
- setDismissedMenuValue(void 0);
31941
+ if (key.meta && input === "b") {
31942
+ moveCursorTo(moveWordLeft(value, cursor));
30879
31943
  return;
30880
31944
  }
30881
- if (key.ctrl && input === "k") {
30882
- setValue(value.slice(0, cursor));
30883
- setDismissedMenuValue(void 0);
31945
+ if (key.meta && input === "f") {
31946
+ moveCursorTo(moveWordRight(value, cursor));
30884
31947
  return;
30885
31948
  }
30886
- if (key.ctrl && input === "l") {
30887
- 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));
30888
31967
  return;
30889
31968
  }
30890
31969
  if (key.ctrl && input === "a") {
30891
- setCursor(0);
31970
+ moveCursorTo(lineBounds(value, cursor).start);
30892
31971
  return;
30893
31972
  }
30894
31973
  if (key.ctrl && input === "e") {
30895
- 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();
30896
31991
  return;
30897
31992
  }
30898
31993
  if (input !== "" && !key.ctrl && !key.meta) {
30899
- setValue(value.slice(0, cursor) + input + value.slice(cursor));
30900
- setCursor(cursor + input.length);
30901
- setCompletionIndex(0);
30902
- 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));
30903
32015
  }
30904
32016
  });
30905
32017
  const [waveTick, setWaveTick] = (0, import_react.useState)(null);
@@ -30940,94 +32052,140 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
30940
32052
  const tierHues = waveTier === null ? null : deepseekWaveHues(waveTier);
30941
32053
  const promptColor = tierHues === null ? inkColor(getPalette().brand) : inkColor(tierHues[0]);
30942
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)));
30943
32074
  if (frozen) {
30944
- if (deleteConfirm !== void 0) return (0, import_react.createElement)(Box, {
30945
- width: Math.max(1, columns - 1),
30946
- borderStyle: "round",
30947
- borderColor: inkColor(getPalette().warn),
30948
- paddingX: 1
30949
- }, (0, import_react.createElement)(Text, { wrap: "truncate-end" }, (0, import_react.createElement)(Text, {
30950
- color: inkColor(getPalette().warn),
30951
- bold: true
30952
- }, "❯ "), (0, import_react.createElement)(Text, {
30953
- color: inkColor(getPalette().warn),
30954
- bold: true
30955
- }, "y delete · any other key cancels")));
30956
- const frozen = value === "" ? "type a message" : verboseLine(value, Math.max(1, columns - 6));
30957
- return (0, import_react.createElement)(Box, {
30958
- width: Math.max(1, columns - 1),
30959
- borderStyle: "round",
30960
- borderColor: inkColor(getPalette().dim),
30961
- paddingX: 1
30962
- }, (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, {
30963
32093
  color: promptColor,
30964
32094
  bold: tierActive ? true : void 0
30965
- }, busy ? "… " : `${promptGlyph} `), frozen));
32095
+ }, busy ? "… " : `${promptGlyph} `), frozenLine, bandFill(2 + visibleColumns(frozenLine))));
30966
32096
  }
30967
- const frame = (row, borderRgb) => (0, import_react.createElement)(Box, {
30968
- width: Math.max(1, columns - 1),
30969
- borderStyle: "round",
30970
- borderColor: inkColor(borderRgb ?? getPalette().dim),
30971
- paddingX: 1
30972
- }, row);
30973
32097
  const menu = (0, import_react.createElement)(CompletionMenu, {
30974
32098
  active: menuActive,
30975
32099
  mention: mentionActive,
30976
32100
  index: completionIndex,
30977
32101
  rows: menuRows
30978
32102
  });
30979
- const editor = editorWindow(value, cursor, Math.max(1, columns - 6));
30980
- const staticRow = (0, import_react.createElement)(Text, { wrap: "truncate-end" }, busy ? (0, import_react.createElement)(BusyChase) : (0, import_react.createElement)(Text, {
30981
- color: promptColor,
30982
- bold: tierActive ? true : void 0
30983
- }, `${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);
30984
32126
  const waveRow = () => {
30985
- const contentWidth = Math.max(1, columns - 5);
30986
- const waveEditor = editorWindow(value, cursor, Math.max(1, contentWidth - 2));
30987
32127
  const hues = deepseekWaveHues(waveTier);
30988
32128
  const style = waveStyle;
30989
- const base = getTheme() === "light" ? WAVE_BASE_LIGHT : WAVE_BASE_DARK;
30990
- const waveBg = (column) => {
30991
- const rgb = deepseekWaveColumnBg(waveTick, column, contentWidth, waveTier, style, hues, base);
30992
- 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);
30993
32133
  };
30994
- const cells = [];
30995
- cells.push({
30996
- char: promptGlyph,
30997
- color: promptColor,
30998
- bold: true,
30999
- backgroundColor: waveBg(0)
31000
- });
31001
- cells.push({
31002
- char: " ",
31003
- color: promptColor,
31004
- backgroundColor: waveBg(1)
31005
- });
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
+ ];
31006
32164
  for (const char of waveEditor.before) cells.push({
31007
32165
  char,
31008
- backgroundColor: waveBg(cells.length)
32166
+ backgroundColor: waveBg(1, cells.length)
31009
32167
  });
31010
32168
  cells.push({
31011
32169
  char: waveEditor.caret,
31012
32170
  inverse: true,
31013
- backgroundColor: waveBg(cells.length)
32171
+ backgroundColor: waveBg(1, cells.length)
31014
32172
  });
31015
32173
  if (value === "" && !busy) for (let at = 0; at < 40; at += 1) cells.push({
31016
32174
  char: COMPOSER_PLACEHOLDER[at],
31017
32175
  dim: true,
31018
- backgroundColor: waveBg(cells.length)
32176
+ backgroundColor: waveBg(1, cells.length)
31019
32177
  });
31020
32178
  else for (const char of waveEditor.after) cells.push({
31021
32179
  char,
31022
- backgroundColor: waveBg(cells.length)
32180
+ backgroundColor: waveBg(1, cells.length)
31023
32181
  });
31024
- while (cells.length < contentWidth) cells.push({
32182
+ while (cells.length < bandWidth) cells.push({
31025
32183
  char: " ",
31026
- backgroundColor: waveBg(cells.length)
32184
+ backgroundColor: waveBg(1, cells.length)
31027
32185
  });
31028
32186
  if (deepseekWaveWordVisible(waveTick, waveTier, style)) {
31029
32187
  const word = waveTier === "unknown" ? "Into the Unknown" : "deepseek";
31030
- const start = Math.max(2, Math.floor((contentWidth - word.length) / 2));
32188
+ const start = Math.max(2, Math.floor((bandWidth - word.length) / 2));
31031
32189
  let clear = true;
31032
32190
  for (let at = 0; at < word.length; at += 1) {
31033
32191
  const cell = cells[start + at];
@@ -31056,24 +32214,19 @@ function Input({ active, frozen, busy, descriptors, skills, dispatch, steer, int
31056
32214
  }
31057
32215
  }
31058
32216
  }
31059
- const borderRgb = deepseekWaveBorderColor(waveTick, waveTier, style, hues, getPalette().dim);
31060
- 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));
31061
32221
  };
31062
- 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));
31063
32223
  }
31064
- /** Build one settled row (row Box plus its roomy-prompt spacers). */
31065
- function buildSettledRow(entry, index, showReasoning) {
31066
- const row = (0, import_react.createElement)(EntryLine, {
31067
- entry,
31068
- showReasoning,
31069
- verbose: false
31070
- });
32224
+ /** Build one settled row (row Box plus its roomy-prompt spacers and row count). */
32225
+ function buildSettledRow(entry, index, showReasoning, columns) {
31071
32226
  const roomyPrompt = entry.kind === "user" && !entry.notice;
32227
+ const lines = settledEntryLines(entry, Math.max(10, columns - 2), showReasoning);
31072
32228
  return {
31073
- box: (0, import_react.createElement)(Box, {
31074
- key: index,
31075
- paddingX: 1
31076
- }, row),
32229
+ box: (0, import_react.createElement)(Box, { key: index }, (0, import_react.createElement)(StyledRows, { lines })),
31077
32230
  before: roomyPrompt ? (0, import_react.createElement)(Box, {
31078
32231
  key: `prompt-before-${index}`,
31079
32232
  paddingX: 1
@@ -31082,10 +32235,17 @@ function buildSettledRow(entry, index, showReasoning) {
31082
32235
  key: `prompt-after-${index}`,
31083
32236
  paddingX: 1
31084
32237
  }, (0, import_react.createElement)(Text, null, " ")) : void 0,
31085
- reasonSensitive: entry.kind === "assistant" && entry.reasoning !== "",
31086
- showReasoning
32238
+ rows: lines.length + (roomyPrompt ? 2 : 0)
31087
32239
  };
31088
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
+ }
31089
32249
  /**
31090
32250
  * The settled `<Static>` row set as a PURE incremental state machine (App
31091
32251
  * drives it from the memo; tests drive it directly and read `built`).
@@ -31099,77 +32259,82 @@ function buildSettledRow(entry, index, showReasoning) {
31099
32259
  * rebuild of rows, Map, or MarkdownBody parses). `records` is mutated in place
31100
32260
  * on the append/toggle paths to stay O(delta).
31101
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
+ *
31102
32277
  * Full rebuilds run only on the rare, deliberate paths: no cache yet, a
31103
- * source-backed replay (`epoch` bump: resize / Ctrl+L / Ctrl+R remounts
31104
- * `<Static>` and must re-flush the CURRENT rows), a `resumed` change, or a shrink
31105
- * (`store.reset`). A reasoning toggle rebuilds only the rows whose text
31106
- * 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.
31107
32283
  */
31108
- function computeSettledRows(previous, entries, settled, showReasoning, resumed, epoch) {
32284
+ function computeSettledRows(previous, entries, settled, showReasoning, resumed, epoch, columns = 80, rowCap = SETTLED_ROW_CAP) {
31109
32285
  if (previous === void 0 || previous.epoch !== epoch || previous.resumed !== resumed || settled < previous.entries.length) {
31110
32286
  const records = /* @__PURE__ */ new Map();
31111
- const flat = [(0, import_react.createElement)(Header, {
31112
- key: "header",
31113
- resumed
31114
- })];
31115
- 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--) {
31116
32292
  const entry = entries[index];
31117
- 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
+ }
31118
32299
  records.set(entry, record);
31119
- if (record.before !== void 0) flat.push(record.before);
31120
- flat.push(record.box);
31121
- 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);
31122
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];
31123
32314
  return {
31124
32315
  cache: {
31125
- entries: entries.slice(0, settled),
32316
+ entries: entries.slice(droppedEntries, settled),
31126
32317
  records,
31127
- header: flat[0],
32318
+ header,
31128
32319
  resumed,
31129
32320
  showReasoning,
31130
32321
  epoch,
31131
- flat
32322
+ columns,
32323
+ flat,
32324
+ droppedEntries,
32325
+ totalRows: windowRows,
32326
+ needsTrim: false
31132
32327
  },
31133
- built: settled
31134
- };
31135
- }
31136
- if (previous.showReasoning !== showReasoning) {
31137
- const records = previous.records;
31138
- const flat = [previous.header];
31139
- let built = 0;
31140
- for (let index = 0; index < previous.entries.length; index++) {
31141
- const entry = previous.entries[index];
31142
- const record = records.get(entry);
31143
- const current = record.reasonSensitive ? {
31144
- ...record,
31145
- box: (0, import_react.createElement)(Box, {
31146
- key: index,
31147
- paddingX: 1
31148
- }, (0, import_react.createElement)(EntryLine, {
31149
- entry,
31150
- showReasoning,
31151
- verbose: false
31152
- })),
31153
- showReasoning
31154
- } : record;
31155
- if (current !== record) {
31156
- records.set(entry, current);
31157
- built += 1;
31158
- }
31159
- if (current.before !== void 0) flat.push(current.before);
31160
- flat.push(current.box);
31161
- if (current.after !== void 0) flat.push(current.after);
31162
- }
31163
- return {
31164
- cache: {
31165
- ...previous,
31166
- records,
31167
- showReasoning,
31168
- flat
31169
- },
31170
- built
32328
+ built: records.size
31171
32329
  };
31172
32330
  }
32331
+ if (previous.showReasoning !== showReasoning) return {
32332
+ cache: {
32333
+ ...previous,
32334
+ showReasoning
32335
+ },
32336
+ built: 0
32337
+ };
31173
32338
  if (settled === previous.entries.length) return {
31174
32339
  cache: previous,
31175
32340
  built: 0
@@ -31177,15 +32342,19 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
31177
32342
  const records = previous.records;
31178
32343
  const suffix = [];
31179
32344
  const added = [];
31180
- 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++) {
31181
32347
  const entry = entries[index];
31182
- const record = buildSettledRow(entry, index, showReasoning);
32348
+ const record = buildSettledRow(entry, index, showReasoning, previous.columns);
31183
32349
  records.set(entry, record);
31184
32350
  suffix.push(entry);
32351
+ deltaRows += record.rows;
31185
32352
  if (record.before !== void 0) added.push(record.before);
31186
32353
  added.push(record.box);
31187
32354
  if (record.after !== void 0) added.push(record.after);
31188
32355
  }
32356
+ const totalRows = previous.totalRows + deltaRows;
32357
+ const needsTrim = rowCap > 0 && totalRows > rowCap + Math.floor(rowCap / 4);
31189
32358
  return {
31190
32359
  cache: {
31191
32360
  entries: previous.entries.concat(suffix),
@@ -31194,9 +32363,13 @@ function computeSettledRows(previous, entries, settled, showReasoning, resumed,
31194
32363
  resumed: previous.resumed,
31195
32364
  showReasoning,
31196
32365
  epoch: previous.epoch,
31197
- flat: previous.flat.concat(added)
32366
+ columns: previous.columns,
32367
+ flat: previous.flat.concat(added),
32368
+ droppedEntries: previous.droppedEntries,
32369
+ totalRows,
32370
+ needsTrim
31198
32371
  },
31199
- built: settled - previous.entries.length
32372
+ built: suffix.length
31200
32373
  };
31201
32374
  }
31202
32375
  /** The whole terminal app; state arrives via the store, output via Ink. */
@@ -31224,7 +32397,7 @@ function App(props) {
31224
32397
  * follows the applied model label (what the status bar actually shows),
31225
32398
  * never the initial paint, and the tier is derived from the label and
31226
32399
  * cached at the switch. The 33ms tick itself lives inside Input, so the
31227
- * sweep re-renders only the composer row, not the whole tree, at 30fps;
32400
+ * sweep re-renders only the composer band, not the whole tree, at 30fps;
31228
32401
  * App owns the rarely-changing tier/style and Input starts the sweep
31229
32402
  * whenever that pair changes. */
31230
32403
  const [waveTier, setWaveTier] = (0, import_react.useState)(null);
@@ -31315,18 +32488,21 @@ function App(props) {
31315
32488
  const busy = view.busy;
31316
32489
  const [showReasoning, setShowReasoning] = (0, import_react.useState)(false);
31317
32490
  const [verboseOpen, setVerboseOpen] = (0, import_react.useState)(false);
32491
+ const [diffView, setDiffView] = (0, import_react.useState)(void 0);
31318
32492
  const [helpOpen, setHelpOpen] = (0, import_react.useState)(false);
31319
32493
  const [modeOpen, setModeOpen] = (0, import_react.useState)(false);
31320
32494
  const [permissionOpen, setPermissionOpen] = (0, import_react.useState)(false);
31321
32495
  const [resumeOpen, setResumeOpen] = (0, import_react.useState)(false);
31322
32496
  const [pluginOpen, setPluginOpen] = (0, import_react.useState)(false);
31323
32497
  const [pluginQuery, setPluginQuery] = (0, import_react.useState)("");
32498
+ const [jobsOpen, setJobsOpen] = (0, import_react.useState)(false);
31324
32499
  const [statuslineOpen, setStatuslineOpen] = (0, import_react.useState)(false);
31325
32500
  const [statuslineItems, setStatuslineItems] = (0, import_react.useState)(() => parseStatuslineItems(props.statusline));
31326
32501
  const [themeOpen, setThemeOpen] = (0, import_react.useState)(false);
31327
32502
  const [historyOpen, setHistoryOpen] = (0, import_react.useState)(false);
31328
32503
  const [agentsOpen, setAgentsOpen] = (0, import_react.useState)(false);
31329
32504
  const [subagentOpen, setSubagentOpen] = (0, import_react.useState)(false);
32505
+ const [todosOpen, setTodosOpen] = (0, import_react.useState)(false);
31330
32506
  /** /delete state: delete-mode hint plus an optional pre-armed row id. */
31331
32507
  const [resumeDelete, setResumeDelete] = (0, import_react.useState)({ mode: false });
31332
32508
  /** The row id awaiting y/n in the COMPOSER (codex delete confirm): the
@@ -31391,7 +32567,7 @@ function App(props) {
31391
32567
  const agentRows = (0, import_react.useSyncExternalStore)(props.subagents.subscribe, props.subagents.getSnapshot);
31392
32568
  const approvalPending = approvalSnapshot.pending !== void 0;
31393
32569
  const questionPending = questionSnapshot.pending !== void 0;
31394
- 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;
31395
32571
  (0, import_react.useEffect)(() => {
31396
32572
  if (!approvalPending && !questionPending) return;
31397
32573
  setModelOpen(false);
@@ -31408,12 +32584,20 @@ function App(props) {
31408
32584
  setHistoryOpen(false);
31409
32585
  setAgentsOpen(false);
31410
32586
  setSubagentOpen(false);
32587
+ setTodosOpen(false);
31411
32588
  setDeleteConfirmId(void 0);
31412
32589
  setVerboseOpen(false);
32590
+ setDiffView(void 0);
31413
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);
31414
32598
  const settledRowsCache = (0, import_react.useRef)(void 0);
31415
32599
  const settledRows = (0, import_react.useMemo)(() => {
31416
- 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);
31417
32601
  settledRowsCache.current = result.cache;
31418
32602
  return result.cache.flat;
31419
32603
  }, [
@@ -31421,14 +32605,10 @@ function App(props) {
31421
32605
  settled,
31422
32606
  showReasoning,
31423
32607
  props.resumed,
31424
- refreshEpoch
32608
+ refreshEpoch,
32609
+ terminalSize.columns
31425
32610
  ]);
31426
- const appStdout = useStdout().stdout;
31427
- const [terminalSize, setTerminalSize] = (0, import_react.useState)(() => ({
31428
- columns: appStdout?.columns ?? 80,
31429
- rows: appStdout?.rows ?? 30
31430
- }));
31431
- const terminalSizeRef = (0, import_react.useRef)(terminalSize);
32611
+ const synchronizedReplayPending = (0, import_react.useRef)(false);
31432
32612
  (0, import_react.useEffect)(() => {
31433
32613
  if (appStdout === void 0) return;
31434
32614
  let replayTimer;
@@ -31442,7 +32622,8 @@ function App(props) {
31442
32622
  setTerminalSize(next);
31443
32623
  if (replayTimer !== void 0) clearTimeout(replayTimer);
31444
32624
  replayTimer = setTimeout(() => {
31445
- 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");
31446
32627
  setRefreshEpoch((epoch) => epoch + 1);
31447
32628
  }, RESIZE_REFLOW_DELAY_MS);
31448
32629
  };
@@ -31455,28 +32636,51 @@ function App(props) {
31455
32636
  const terminalRows = terminalSize.rows;
31456
32637
  const terminalColumns = terminalSize.columns;
31457
32638
  const composerGutterRows = layoutGutterRows(terminalRows);
31458
- 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));
31459
32645
  const streamingActive = view.streaming !== "" || view.streamingReasoning !== "";
31460
32646
  const deepDivingVisible = busy && !streamingActive;
31461
- 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)), [
31462
32648
  view.entries,
31463
32649
  settled,
31464
- terminalColumns
32650
+ terminalColumns,
32651
+ showReasoning
31465
32652
  ]);
31466
- 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));
31467
32654
  const visibleLiveLines = liveBudget === 0 ? [] : allLiveLines.slice(-liveBudget);
31468
32655
  const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length);
31469
32656
  const reasoningRows = view.streamingReasoning === "" ? 0 : view.streaming === "" ? streamRows : streamRows <= 1 ? 0 : showReasoning ? Math.max(1, Math.floor(streamRows / 3)) : 1;
31470
32657
  const answerRows = view.streaming === "" ? 0 : Math.max(1, streamRows - reasoningRows);
31471
- const transcriptVisible = !modelOpen && !helpOpen && !modeOpen && !permissionOpen && !resumeOpen && !pluginOpen && !statuslineOpen && !themeOpen && !historyOpen && !agentsOpen && !subagentOpen && !verboseOpen && !approvalPending && !questionPending;
31472
- 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;
31473
32660
  const closeInspector = (0, import_react.useCallback)(() => {
31474
32661
  setVerboseOpen(false);
31475
32662
  }, []);
31476
32663
  const refreshScreen = () => {
31477
- 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
+ }
31478
32668
  setRefreshEpoch((epoch) => epoch + 1);
31479
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
+ ]);
31480
32684
  /** Apply one /model pick: record the selection, close the panel, report via notice. */
31481
32685
  const applyModel = (row, effortId) => {
31482
32686
  try {
@@ -31503,7 +32707,20 @@ function App(props) {
31503
32707
  };
31504
32708
  let modelSurface;
31505
32709
  if (modelOpen && !approvalPending && !questionPending) {
31506
- 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, {
31507
32724
  target: providerAction.target,
31508
32725
  save: props.saveModelProviderCredential,
31509
32726
  done: () => {
@@ -31554,6 +32771,16 @@ function App(props) {
31554
32771
  target
31555
32772
  });
31556
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
+ },
31557
32784
  onUnset: (target) => {
31558
32785
  if (props.unsetModelProviderCredential === void 0) {
31559
32786
  notify("API key removal is unavailable in this profile", "warning");
@@ -31604,11 +32831,8 @@ function App(props) {
31604
32831
  return (0, import_react.createElement)(Box, { flexDirection: "column" }, (0, import_react.createElement)(MemoStaticTranscript, {
31605
32832
  key: refreshEpoch,
31606
32833
  items: settledRows
31607
- }), transcriptVisible ? (0, import_react.createElement)(Box, {
31608
- flexDirection: "column",
31609
- paddingX: 2
31610
- }, visibleLiveLines.length === 0 ? void 0 : (0, import_react.createElement)(StyledRows, { lines: visibleLiveLines }), view.streamingReasoning !== "" && reasoningRows > 0 ? (0, import_react.createElement)(StreamTail, {
31611
- 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)",
31612
32836
  prefix: "✻ ",
31613
32837
  continuationPrefix: " ",
31614
32838
  dim: true,
@@ -31618,7 +32842,12 @@ function App(props) {
31618
32842
  dim: false,
31619
32843
  maxRows: answerRows,
31620
32844
  prefix: " "
31621
- }, 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, {
31622
32851
  store: props.questions,
31623
32852
  snapshot: questionSnapshot,
31624
32853
  locked: false
@@ -31634,6 +32863,9 @@ function App(props) {
31634
32863
  onClose: () => {
31635
32864
  setHelpOpen(false);
31636
32865
  }
32866
+ }) : void 0, diffView !== void 0 && !approvalPending && !questionPending ? (0, import_react.createElement)(DiffPanel, {
32867
+ view: diffView,
32868
+ onClose: () => setDiffView(void 0)
31637
32869
  }) : void 0, verboseOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(MemoVerbosePanel, {
31638
32870
  entries: view.entries,
31639
32871
  onClose: closeInspector
@@ -31677,6 +32909,9 @@ function App(props) {
31677
32909
  load: props.loadPlugins,
31678
32910
  initialQuery: pluginQuery,
31679
32911
  close: () => setPluginOpen(false)
32912
+ }) : void 0, jobsOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(JobsPanel, {
32913
+ load: props.loadJobs,
32914
+ close: () => setJobsOpen(false)
31680
32915
  }) : void 0, statuslineOpen && !approvalPending && !questionPending ? (0, import_react.createElement)(StatuslinePanel, {
31681
32916
  enabled: statuslineItems,
31682
32917
  change: (items) => {
@@ -31790,11 +33025,13 @@ function App(props) {
31790
33025
  setPluginQuery(query);
31791
33026
  setPluginOpen(true);
31792
33027
  },
33028
+ openJobs: () => setJobsOpen(true),
31793
33029
  openStatusline: () => setStatuslineOpen(true),
31794
33030
  openTheme: () => setThemeOpen(true),
31795
33031
  openHistory: () => setHistoryOpen(true),
31796
33032
  openAgents: () => setAgentsOpen(true),
31797
33033
  openSubagent: () => setSubagentOpen(true),
33034
+ openTodos: () => setTodosOpen(true),
31798
33035
  openDelete: (id) => {
31799
33036
  const armed = id === void 0 || id === "" ? void 0 : id;
31800
33037
  setResumeDelete({
@@ -31804,10 +33041,17 @@ function App(props) {
31804
33041
  setDeleteConfirmId(armed);
31805
33042
  setResumeOpen(true);
31806
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,
31807
33050
  deleteConfirm: deleteConfirmId,
31808
33051
  confirmDelete,
31809
33052
  cancelDelete,
31810
33053
  createSession: props.createSession,
33054
+ forkSession: props.forkSession,
31811
33055
  cancelSessionSwitch: props.cancelSessionSwitch,
31812
33056
  notify,
31813
33057
  hasNotice: notice !== void 0,
@@ -31823,12 +33067,13 @@ function App(props) {
31823
33067
  refresh: refreshScreen,
31824
33068
  toggleReasoning: () => {
31825
33069
  setShowReasoning((current) => !current);
31826
- refreshScreen();
33070
+ if (!busy && !streamingActive) refreshScreen();
31827
33071
  },
31828
33072
  loadMentions: props.loadMentions,
31829
33073
  cyclePermission: props.cyclePermission,
31830
33074
  exportTranscript: props.exportTranscript,
31831
33075
  renameTitle: props.renameTitle,
33076
+ copyLastResponse: props.copyLastResponse,
31832
33077
  recallSpace,
31833
33078
  recordLocal,
31834
33079
  recordHistory: props.recordHistory,
@@ -31837,7 +33082,9 @@ function App(props) {
31837
33082
  historyFill,
31838
33083
  historyConsumed,
31839
33084
  waveTier,
31840
- waveStyle
33085
+ waveStyle,
33086
+ maxRows: composerEditorCap,
33087
+ onEditorRows: handleEditorRows
31841
33088
  }), (0, import_react.createElement)(StatusLine, {
31842
33089
  facts: {
31843
33090
  model: modelLabel,
@@ -32024,13 +33271,15 @@ function isSlashLine(line) {
32024
33271
  /** Substitutable runner effects; production values write to the real terminal. */
32025
33272
  const internals = {
32026
33273
  mount: (element) => {
32027
- const instance = render(element);
33274
+ process.stdout.write("\x1B[>4;0m\x1B[>5u\x1B[?2004h");
33275
+ const instance = render(element, { exitOnCtrlC: false });
32028
33276
  return {
32029
33277
  rerender(element) {
32030
33278
  instance.rerender(element);
32031
33279
  },
32032
33280
  unmount() {
32033
33281
  instance.unmount();
33282
+ process.stdout.write("\x1B[<u\x1B[>4;0m\x1B[?2004l");
32034
33283
  }
32035
33284
  };
32036
33285
  },
@@ -32221,6 +33470,26 @@ function profileRefOf(profile) {
32221
33470
  const ref = profile.apiKeyEnv;
32222
33471
  return typeof ref === "string" && ref.length > 0 ? ref : void 0;
32223
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
+ }
32224
33493
  /**
32225
33494
  * The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
32226
33495
  * with the route uppercased and every non-alphanumeric run collapsed to one
@@ -32323,6 +33592,7 @@ async function loadProviderSettings(ctx) {
32323
33592
  settingsRevision: namespace?.revision ?? 0,
32324
33593
  configured,
32325
33594
  removable,
33595
+ configuration: configurationOf(profile),
32326
33596
  ...credentialRef === void 0 ? {} : { credentialRef },
32327
33597
  suggestedRef: deriveCredentialRef(base.provider),
32328
33598
  ...base.declared === void 0 ? {} : { declared: base.declared }
@@ -32408,6 +33678,50 @@ async function saveProviderCredential(ctx, target, rawKey) {
32408
33678
  throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
32409
33679
  }
32410
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
+ }
32411
33725
  /**
32412
33726
  * Remove the currently named credential without touching the provider
32413
33727
  * profile. Only the resolved profile's own reference is unset; a dormant or
@@ -32468,133 +33782,43 @@ async function removeProviderSettings(ctx, target) {
32468
33782
  }
32469
33783
  //#endregion
32470
33784
  //#region src/mentions.ts
32471
- /**
32472
- * Workspace @mention support: file and directory candidates from a bounded
32473
- * async scan of the session cwd, session candidates from the opt-in
32474
- * `sessionReferenceResolver` service, and submission preparation through its
32475
- * `prepare()` API. Picked session mentions land as canonical
32476
- * `@[label](dsh-session:…)` tokens; on submit the text is parsed back into
32477
- * readable `@label` text plus structured references, snapshots are injected
32478
- * via `agent.inject()` before the readable message wakes the driver
32479
- * (`followup` idle, `steer` running) — exactly the upstream README's wiring.
32480
- *
32481
- * Harness exposes no workspace-file mention service (only session references
32482
- * plus a post-hoc produced-file linker), so the file index is the lightweight
32483
- * bounded scan below, kept deliberately smaller than Codex's streaming
32484
- * gitignore-aware walker.
32485
- *
32486
- * @module @deepseek-ai/dsh-code/mentions
32487
- */
32488
- /** Directories never entered and files never listed during the scan. */
32489
- const SKIP_DIRS = /* @__PURE__ */ new Set([
32490
- ".git",
32491
- "node_modules",
32492
- "lib",
32493
- "dist",
32494
- "out",
32495
- ".omc",
32496
- "coverage"
32497
- ]);
32498
- const MAX_FILES = 4e3;
32499
- const MAX_DEPTH = 12;
32500
- /** Empty-query default rows: proves the index exists without typing (Codex's
32501
- * popups show something on a bare sigil too). */
32502
- const EMPTY_QUERY_ROWS = 20;
32503
- /**
32504
- * Bounded async BFS scan of a workspace; unreadable entries are skipped.
32505
- * Both files and directories are indexed (directories insert with a trailing
32506
- * slash), mirroring Codex's `MatchType::{File,Directory}` index. Dotfiles and
32507
- * the {@link SKIP_DIRS} list are excluded, which is a coarser filter than
32508
- * Codex's gitignore-aware walker but stays dependency-free and bounded.
32509
- */
32510
- async function scanWorkspaceFiles(root, signal) {
32511
- const found = [];
32512
- const pending = [{
32513
- absolute: root,
32514
- relative: "",
32515
- depth: 0
32516
- }];
32517
- const aborted = () => signal?.aborted === true;
32518
- while (pending.length > 0 && found.length < MAX_FILES && !aborted()) {
32519
- const current = pending.shift();
32520
- if (current === void 0) break;
32521
- let entries;
32522
- try {
32523
- entries = await readdir(current.absolute, { withFileTypes: true });
32524
- } catch {
32525
- continue;
32526
- }
32527
- for (const entry of entries) {
32528
- if (found.length >= MAX_FILES || aborted()) return found;
32529
- if (entry.name.startsWith(".")) continue;
32530
- const relative = current.relative === "" ? entry.name : `${current.relative}/${entry.name}`;
32531
- if (entry.isDirectory()) {
32532
- if (SKIP_DIRS.has(entry.name) || current.depth + 1 > MAX_DEPTH) continue;
32533
- found.push({
32534
- path: relative,
32535
- kind: "directory"
32536
- });
32537
- pending.push({
32538
- absolute: join(current.absolute, entry.name),
32539
- relative,
32540
- depth: current.depth + 1
32541
- });
32542
- } else if (entry.isFile()) found.push({
32543
- path: relative,
32544
- kind: "file"
32545
- });
32546
- }
32547
- }
32548
- return found.sort((left, right) => left.path < right.path ? -1 : 1);
32549
- }
32550
- /** True when every query character appears in order in the haystack. */
32551
- function isSubsequence(query, haystack) {
32552
- let at = 0;
32553
- for (const char of haystack) {
32554
- if (char === query[at]) at += 1;
32555
- if (at >= query.length) return true;
32556
- }
32557
- return at >= query.length;
32558
- }
32559
- /** Rank one file path against the typed query (community-TUI scoring shape). */
32560
- function scoreFile(path, query) {
32561
- const name = path.slice(path.lastIndexOf("/") + 1);
32562
- if (query === "") return 0;
32563
- if (name === query) return 1e3;
32564
- if (name.startsWith(query)) return 900;
32565
- if (name.includes(query)) return 700;
32566
- if (path.includes(query)) return 500;
32567
- if (isSubsequence(query, name)) return 300;
32568
- return 0;
32569
- }
33785
+ /** Menu cap on file rows; the service owns ranking and default rows. */
33786
+ const MAX_FILE_ROWS = 20;
32570
33787
  /**
32571
33788
  * Create the mention API for one agent's workspace. A missing
32572
- * session-reference service degrades to file mentions only (the scan still
32573
- * works); `prepare` then passes text through untouched. An undefined agent
32574
- * (a bare launch before any session exists) also degrades to file-only
32575
- * 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.
32576
33795
  *
32577
- * `files` is hoisted into the closure so `candidates` never reaches for
32578
- * `this` the runner hands `mentions.candidates` to the input editor as a
32579
- * detached callback, and a `this`-bound method would throw on every `@` key.
32580
- * @param ctx - context carrying the optional `sessionReferenceResolver`.
32581
- * @param agent - the session owner; excluded from its own candidates.
32582
- * @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.
32583
33803
  */
32584
33804
  function createMentions(ctx, agent, cwd) {
32585
33805
  const resolver = ctx.get("sessionReferenceResolver");
33806
+ const fileReferences = ctx.get("fileReferences");
32586
33807
  const sessionCapable = agent !== void 0 && resolver !== void 0;
32587
- let filesPromise;
32588
- const files = () => {
32589
- filesPromise ??= scanWorkspaceFiles(cwd);
32590
- 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);
32591
33816
  };
32592
33817
  return {
32593
- files,
32594
33818
  async candidates(query, signal) {
32595
33819
  const needle = query.trim();
32596
- const [scanned, sessions] = await Promise.all([files(), sessionCapable && needle !== "" && agent !== void 0 ? resolver.listCandidates(agent, needle, 10, signal).catch(() => []) : Promise.resolve([])]);
32597
- 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) => ({
32598
33822
  label: candidate.path,
32599
33823
  description: candidate.kind === "directory" ? "Folder" : "File",
32600
33824
  kind: candidate.kind
@@ -32720,6 +33944,8 @@ function mountQuestionProvider(ctx) {
32720
33944
  }
32721
33945
  //#endregion
32722
33946
  //#region src/store.ts
33947
+ /** Render frame budget: the notification cadence's upper bound. */
33948
+ const NOTIFY_FRAME_MS$1 = 16;
32723
33949
  /**
32724
33950
  * Create one transcript store, optionally seeded with replayed history. The
32725
33951
  * seed folds synchronously BEFORE the first render, so a resumed session
@@ -32734,13 +33960,18 @@ function createTranscriptStore(replay) {
32734
33960
  let view = replay === void 0 ? createTranscriptView() : projectEvents(replay);
32735
33961
  const listeners = /* @__PURE__ */ new Set();
32736
33962
  let scheduled = false;
33963
+ let lastNotifyAt = 0;
32737
33964
  const notify = () => {
32738
33965
  if (scheduled) return;
32739
33966
  scheduled = true;
32740
- queueMicrotask(() => {
33967
+ const wait = NOTIFY_FRAME_MS$1 - (Date.now() - lastNotifyAt);
33968
+ const dispatch = () => {
32741
33969
  scheduled = false;
33970
+ lastNotifyAt = Date.now();
32742
33971
  for (const listener of listeners) listener();
32743
- });
33972
+ };
33973
+ if (wait <= 0) setImmediate(dispatch);
33974
+ else setTimeout(dispatch, wait);
32744
33975
  };
32745
33976
  return {
32746
33977
  getView: () => view,
@@ -32762,6 +33993,8 @@ function createTranscriptStore(replay) {
32762
33993
  }
32763
33994
  };
32764
33995
  }
33996
+ /** Render frame budget: the notification cadence's upper bound. */
33997
+ const NOTIFY_FRAME_MS = 16;
32765
33998
  /** Bounded last-activity text (plain characters, display-sliced later). */
32766
33999
  const MAX_ACTIVITY_CHARS = 80;
32767
34000
  /** Single-line bounded preview of an assembled message's text content. */
@@ -32875,13 +34108,18 @@ function createSubagentFeed() {
32875
34108
  let rows = Object.freeze([]);
32876
34109
  const listeners = /* @__PURE__ */ new Set();
32877
34110
  let scheduled = false;
34111
+ let lastNotifyAt = 0;
32878
34112
  const notify = () => {
32879
34113
  if (scheduled) return;
32880
34114
  scheduled = true;
32881
- queueMicrotask(() => {
34115
+ const wait = NOTIFY_FRAME_MS - (Date.now() - lastNotifyAt);
34116
+ const dispatch = () => {
32882
34117
  scheduled = false;
34118
+ lastNotifyAt = Date.now();
32883
34119
  for (const listener of listeners) listener();
32884
- });
34120
+ };
34121
+ if (wait <= 0) setImmediate(dispatch);
34122
+ else setTimeout(dispatch, wait);
32885
34123
  };
32886
34124
  return {
32887
34125
  apply(sessionId, event) {
@@ -32999,7 +34237,7 @@ function buildExportMarkdown(view, sessionId) {
32999
34237
  for (const entry of view.entries) switch (entry.kind) {
33000
34238
  case "user":
33001
34239
  if (entry.notice) out.push(`> ⤷ context: ${entry.text}`, "");
33002
- else out.push("## user", "", entry.text, "");
34240
+ else out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
33003
34241
  break;
33004
34242
  case "assistant":
33005
34243
  if (entry.reasoning !== "") out.push("<details><summary>thinking</summary>", "", entry.reasoning, "", "</details>", "");
@@ -33032,7 +34270,7 @@ function buildExportMarkdown(view, sessionId) {
33032
34270
  out.push(`> files changed: ${entry.paths.join(", ")}`, "");
33033
34271
  break;
33034
34272
  case "pending":
33035
- out.push("## user", "", entry.text, "");
34273
+ out.push("## user", "", entry.text, ...imageLabels(entry.images) === "" ? [] : [imageLabels(entry.images)], "");
33036
34274
  break;
33037
34275
  default: assertNever(entry, "transcript entry kind");
33038
34276
  }
@@ -33046,6 +34284,212 @@ function buildExportMarkdown(view, sessionId) {
33046
34284
  return out.join("\n");
33047
34285
  }
33048
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
33049
34493
  //#region src/session-switch.ts
33050
34494
  var SessionSwitchQueue = class {
33051
34495
  execute;
@@ -33219,7 +34663,9 @@ const Config = z.object({ startup: z.object({
33219
34663
  kind: z.string().required(),
33220
34664
  sessionId: z.string(),
33221
34665
  mode: z.string(),
33222
- theme: z.string()
34666
+ theme: z.string(),
34667
+ prompt: z.string(),
34668
+ images: z.array(z.string())
33223
34669
  }) });
33224
34670
  /** Report an unexpected direct-driver failure and request a failing exit. */
33225
34671
  function fail(io, error) {
@@ -33227,6 +34673,33 @@ function fail(io, error) {
33227
34673
  io.exit(1);
33228
34674
  }
33229
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
+ /**
33230
34703
  * Resolve the working directory's git branch for the status line.
33231
34704
  * @param cwd - the session's working directory.
33232
34705
  * @returns the branch name, or '' outside a repository or on a detached HEAD.
@@ -33391,21 +34864,23 @@ async function run(ctx, startup, io) {
33391
34864
  sessionId: SessionId(next.sessionId),
33392
34865
  meta: {
33393
34866
  cwd: nextCwd,
33394
- agentPreset: mode
34867
+ agentPreset: mode,
34868
+ ...next.parentSession === void 0 ? {} : { parentSession: next.parentSession },
34869
+ ...next.seedLength === void 0 ? {} : { seedLength: next.seedLength }
33395
34870
  },
34871
+ ...next.seed === void 0 ? {} : { seed: next.seed },
33396
34872
  agentOptions: seedOptions,
33397
34873
  signal: quitAbort.signal,
33398
34874
  setup
33399
34875
  });
33400
34876
  const session = handle.agent.session;
33401
34877
  if (!next.resume && permissionPresets !== void 0) applyPendingPermission(permissionPresets, session, pendingPermission);
33402
- const sessionCwd = session.header.cwd ?? nextCwd;
33403
34878
  return {
33404
34879
  handle,
33405
34880
  agent: handle.agent,
33406
34881
  session,
33407
34882
  store: createTranscriptStore(session.events),
33408
- mentions: createMentions(ctx, handle.agent, sessionCwd),
34883
+ mentions: createMentions(ctx, handle.agent, session.header.cwd ?? nextCwd),
33409
34884
  mode: mode ?? "standard",
33410
34885
  selection: selectionState,
33411
34886
  resumed: next.resume
@@ -33471,7 +34946,7 @@ async function run(ctx, startup, io) {
33471
34946
  store.apply(event);
33472
34947
  return;
33473
34948
  }
33474
- 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);
33475
34950
  });
33476
34951
  const commands = watchCommands(ctx);
33477
34952
  if (agent !== void 0) commands.setAgent(agent);
@@ -33597,7 +35072,7 @@ async function run(ctx, startup, io) {
33597
35072
  const finish = () => {
33598
35073
  pendingControllers.delete(controller);
33599
35074
  };
33600
- Promise.resolve().then(() => registry.execute(currentAgent, line, controller.signal)).then((execution) => {
35075
+ Promise.resolve().then(() => registry.execute(currentAgent, line, [], controller.signal)).then((execution) => {
33601
35076
  finish();
33602
35077
  if (epoch !== atEpoch || agent !== currentAgent) return;
33603
35078
  if (execution === void 0) try {
@@ -33618,10 +35093,10 @@ async function run(ctx, startup, io) {
33618
35093
  });
33619
35094
  };
33620
35095
  /** Deliver one trimmed line to the live session, expanding mentions first. */
33621
- const deliverLine = (line, mode) => {
35096
+ const deliverLine = (line, mode, images = []) => {
33622
35097
  const currentAgent = agent;
33623
35098
  const currentMentions = mentions;
33624
- if (isSlashLine(line) && mode === "followup") {
35099
+ if (images.length === 0 && isSlashLine(line) && mode === "followup") {
33625
35100
  runSlash(line);
33626
35101
  return;
33627
35102
  }
@@ -33637,11 +35112,12 @@ async function run(ctx, startup, io) {
33637
35112
  if (epoch !== atEpoch || agent !== currentAgent) return;
33638
35113
  try {
33639
35114
  if (context !== void 0) currentAgent.inject(context);
35115
+ const content = [...readable === "" ? [] : [{
35116
+ type: "text",
35117
+ text: readable
35118
+ }], ...images];
33640
35119
  const message = createUserMessage({
33641
- content: [{
33642
- type: "text",
33643
- text: readable
33644
- }],
35120
+ content,
33645
35121
  source: { kind: "user" }
33646
35122
  });
33647
35123
  if (mode === "steer") currentAgent.steer(message);
@@ -33675,7 +35151,7 @@ async function run(ctx, startup, io) {
33675
35151
  await pendingModeWork;
33676
35152
  if (session !== void 0) {
33677
35153
  const queued = pendingInputs.splice(0);
33678
- for (const item of queued) deliverLine(item.text, item.mode);
35154
+ for (const item of queued) deliverLine(item.text, item.mode, item.images);
33679
35155
  return;
33680
35156
  }
33681
35157
  const next = await prepare({
@@ -33702,7 +35178,7 @@ async function run(ctx, startup, io) {
33702
35178
  abortPendingControllers();
33703
35179
  epoch += 1;
33704
35180
  const queued = pendingInputs.splice(0);
33705
- for (const item of queued) deliverLine(item.text, item.mode);
35181
+ for (const item of queued) deliverLine(item.text, item.mode, item.images);
33706
35182
  } finally {
33707
35183
  creating = false;
33708
35184
  }
@@ -33712,14 +35188,14 @@ async function run(ctx, startup, io) {
33712
35188
  });
33713
35189
  };
33714
35190
  /** Deliver one readable line to the agent, expanding session mentions first. */
33715
- const send = (text, mode) => {
35191
+ const send = (text, mode, images = []) => {
33716
35192
  const line = text.trim();
33717
- if (line === "") return;
33718
- if (line.startsWith("/mode ")) {
35193
+ if (line === "" && images.length === 0) return;
35194
+ if (images.length === 0 && line.startsWith("/mode ")) {
33719
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"));
33720
35196
  return;
33721
35197
  }
33722
- if (line.startsWith("/permission ")) {
35198
+ if (images.length === 0 && line.startsWith("/permission ")) {
33723
35199
  try {
33724
35200
  const selected = setPermissionAction(line.slice(12).trim());
33725
35201
  bridge.notify(`permission → ${selected}`);
@@ -33731,12 +35207,13 @@ async function run(ctx, startup, io) {
33731
35207
  if (session === void 0) {
33732
35208
  pendingInputs.push({
33733
35209
  text: line,
33734
- mode
35210
+ mode,
35211
+ images
33735
35212
  });
33736
35213
  ensureSession();
33737
35214
  return;
33738
35215
  }
33739
- deliverLine(line, mode);
35216
+ deliverLine(line, mode, images);
33740
35217
  };
33741
35218
  /** Dispatch one submitted line: slash commands to the registry, other text to the agent. */
33742
35219
  const dispatch = (text) => {
@@ -34068,6 +35545,47 @@ async function run(ctx, startup, io) {
34068
35545
  label: id.slice(-12)
34069
35546
  });
34070
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
+ };
34071
35589
  const switchSession = (row) => {
34072
35590
  if (!row.resumable) {
34073
35591
  bridge.notify("subagent conversations are read-only", "warning");
@@ -34116,6 +35634,7 @@ async function run(ctx, startup, io) {
34116
35634
  loadModelProviders: () => loadProviderSettings(ctx),
34117
35635
  subscribeModelProviders: (listener) => subscribeProviderSettings(ctx, listener),
34118
35636
  saveModelProviderCredential: (target, key) => saveProviderCredential(ctx, target, key),
35637
+ saveModelProviderConfiguration: (target, configuration) => saveProviderConfiguration(ctx, target, configuration),
34119
35638
  unsetModelProviderCredential: (target) => unsetProviderCredential(ctx, target),
34120
35639
  removeModelProvider: (target) => removeProviderSettings(ctx, target),
34121
35640
  loadMentions: (query, signal) => mentions.candidates(query, signal),
@@ -34128,10 +35647,14 @@ async function run(ctx, startup, io) {
34128
35647
  deleteSession,
34129
35648
  exportTranscript,
34130
35649
  renameTitle,
35650
+ copyLastResponse,
35651
+ loadGitDiff: (argument) => loadGitDiff(session?.header.cwd ?? cwd, argument),
35652
+ reviewChanges,
34131
35653
  loadPresets: () => presets.list(),
34132
35654
  switchMode: switchModeAction,
34133
35655
  loadPermissions: () => permissionPresets === void 0 ? Promise.reject(/* @__PURE__ */ new Error("permission presets are not mounted in this composition")) : Promise.resolve(listPermissionRows(permissionPresets)),
34134
35656
  createSession,
35657
+ forkSession,
34135
35658
  loadSessions,
34136
35659
  loadSessionTranscript,
34137
35660
  loadSubagents: () => {
@@ -34143,11 +35666,12 @@ async function run(ctx, startup, io) {
34143
35666
  sort: "newest",
34144
35667
  currentCwd: current.header.cwd ?? cwd,
34145
35668
  query: ""
34146
- }).then((rows) => rows.filter((row) => row.parent === current.id));
35669
+ }).then((rows) => rows.filter((row) => row.parent === current.id && row.subagent));
34147
35670
  },
34148
35671
  switchSession,
34149
35672
  cancelSessionSwitch,
34150
35673
  loadPlugins: () => listPluginRows(ctx),
35674
+ loadJobs: () => listJobs(ctx, active?.agent),
34151
35675
  statusline: statuslineItems,
34152
35676
  saveStatusline,
34153
35677
  saveTheme,
@@ -34163,6 +35687,13 @@ async function run(ctx, startup, io) {
34163
35687
  mountRef.current?.rerender(appElement());
34164
35688
  };
34165
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
+ }
34166
35697
  if (statuslineWarning !== void 0) setTimeout(() => {
34167
35698
  bridge.notify("statusline config unreadable, using defaults: " + statuslineWarning, "warning");
34168
35699
  }, 50);
@@ -34177,22 +35708,27 @@ async function run(ctx, startup, io) {
34177
35708
  */
34178
35709
  function apply(ctx, config) {
34179
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
+ };
34180
35716
  const startup = config.startup.kind === "resume" && config.startup.sessionId !== void 0 ? {
34181
35717
  kind: "resume",
34182
35718
  sessionId: config.startup.sessionId,
34183
- ...theme === void 0 ? {} : { theme }
35719
+ ...input
34184
35720
  } : config.startup.kind === "latest" ? {
34185
35721
  kind: "latest",
34186
- ...theme === void 0 ? {} : { theme }
35722
+ ...input
34187
35723
  } : config.startup.kind === "named" && config.startup.sessionId !== void 0 ? {
34188
35724
  kind: "named",
34189
35725
  sessionId: config.startup.sessionId,
34190
35726
  ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
34191
- ...theme === void 0 ? {} : { theme }
35727
+ ...input
34192
35728
  } : {
34193
35729
  kind: "fresh",
34194
35730
  ...config.startup.mode === void 0 ? {} : { mode: config.startup.mode },
34195
- ...theme === void 0 ? {} : { theme }
35731
+ ...input
34196
35732
  };
34197
35733
  const exit = ctx.get("appExit");
34198
35734
  if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");