@selesai/code 0.9.15 → 0.10.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 (267) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,1290 @@
1
+ import { CustomEditor, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ type AutocompleteProvider,
4
+ type Component,
5
+ type EditorComponent,
6
+ type EditorTheme,
7
+ type TUI,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ } from "@earendil-works/pi-tui";
11
+ import { ACCENT_RAIL_CHROME_WIDTH, renderAccentRailEditorFrame } from "./accent-rail-editor";
12
+ import { renderCompletionPalette } from "./completion-menu";
13
+ import type { EditorStyle, ZentuiConfig } from "./config";
14
+ import {
15
+ type EditorMetadataZones,
16
+ renderEditorMetadataFormatSplit,
17
+ } from "./editor-metadata-format";
18
+ import { type MinimalistEditorMetadata, renderMinimalistFrame } from "./minimalist-editor";
19
+ import {
20
+ EDITOR_ACCENT_FALLBACK,
21
+ EDITOR_BORDER_FALLBACK,
22
+ renderStyleForSourceOrFallback,
23
+ safeThemeFg,
24
+ } from "./style";
25
+
26
+ const LEGACY_SPLIT_POLISHED_FRAME = Symbol.for("pi-zentui.polished-frame");
27
+
28
+ export type ViewportCounts = {
29
+ above?: string;
30
+ below?: string;
31
+ };
32
+
33
+ type PolishedFrameSplit = {
34
+ editorLines: string[];
35
+ /** Logical, unframed autocomplete payload rows owned by this module. */
36
+ trailingLines: string[];
37
+ viewport: ViewportCounts;
38
+ };
39
+
40
+ type PolishedFrameProvenance = {
41
+ rows: readonly string[];
42
+ split: PolishedFrameSplit;
43
+ };
44
+
45
+ const POLISHED_FRAME_SPLITS = new WeakMap<string[], PolishedFrameProvenance>();
46
+
47
+ type AutocompleteListInternals = Pick<Component, "render">;
48
+
49
+ type AutocompleteEditorInternals = {
50
+ autocompleteList?: AutocompleteListInternals;
51
+ isShowingAutocomplete?: () => boolean;
52
+ };
53
+
54
+ type AutocompleteCapture = {
55
+ compatible: boolean;
56
+ called: number;
57
+ rows: string[];
58
+ };
59
+
60
+ type WrappedEditor = EditorComponent &
61
+ AutocompleteEditorInternals & {
62
+ focused?: boolean;
63
+ onEscape?: () => void;
64
+ onCtrlD?: () => void;
65
+ onPasteImage?: () => void;
66
+ onExtensionShortcut?: (data: string) => boolean;
67
+ actionHandlers?: Map<unknown, () => void>;
68
+ wantsKeyRelease?: boolean;
69
+ disableSubmit?: boolean;
70
+ getLines?: () => string[];
71
+ getCursor?: () => unknown;
72
+ getMode?: () => unknown;
73
+ getPaddingX?: () => number;
74
+ getAutocompleteMaxVisible?: () => number;
75
+ addToHistory?: (text: string) => void;
76
+ getExpandedText?: () => string;
77
+ insertTextAtCursor?: (text: string) => void;
78
+ setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
79
+ setPaddingX?: (padding: number) => void;
80
+ setAutocompleteMaxVisible?: (maxVisible: number) => void;
81
+ };
82
+
83
+ export type EditorMeta = {
84
+ modelLabel: string;
85
+ modelId?: string;
86
+ modelName?: string;
87
+ providerLabel: string;
88
+ sessionName?: string;
89
+ contextPercent?: number;
90
+ contextWindow?: number;
91
+ inputTokens?: number;
92
+ outputTokens?: number;
93
+ cacheHitRate?: number;
94
+ };
95
+
96
+ export type PolishedEditorFrameOptions = {
97
+ width: number;
98
+ editorLines: string[];
99
+ autocompleteLines?: string[];
100
+ viewport?: ViewportCounts;
101
+ uiTheme: Theme;
102
+ config: ZentuiConfig;
103
+ modelMeta: EditorMeta;
104
+ thinkingLevel?: string;
105
+ rightStatus?: string;
106
+ borderColor?: (text: string) => string;
107
+ };
108
+
109
+ type PolishedFrameOptions = {
110
+ width: number;
111
+ baseRendered: string[];
112
+ autocompleteSource: AutocompleteEditorInternals;
113
+ autocompleteCapture?: AutocompleteCapture;
114
+ uiTheme: Theme;
115
+ config: ZentuiConfig;
116
+ modelMeta: EditorMeta;
117
+ thinkingLevel: string | undefined;
118
+ rightStatus?: string;
119
+ ownedFrame?: PolishedFrameSplit;
120
+ trustedBaseFrame?: boolean;
121
+ borderColor?: (text: string) => string;
122
+ };
123
+
124
+ type PolishedFrameResult = {
125
+ lines: string[];
126
+ decorated: boolean;
127
+ };
128
+
129
+ type AccentRailFrameAdapterOptions = {
130
+ width: number;
131
+ baseRendered: string[];
132
+ autocompleteSource: AutocompleteEditorInternals;
133
+ autocompleteCapture?: AutocompleteCapture;
134
+ uiTheme: Theme;
135
+ config: ZentuiConfig;
136
+ ownedFrame?: PolishedFrameSplit;
137
+ trustedBaseFrame?: boolean;
138
+ };
139
+
140
+ type MinimalistFrameAdapterOptions = {
141
+ width: number;
142
+ baseRendered: string[];
143
+ autocompleteSource: AutocompleteEditorInternals;
144
+ autocompleteCapture?: AutocompleteCapture;
145
+ uiTheme: Theme;
146
+ config: ZentuiConfig;
147
+ inputText: string;
148
+ metadata: MinimalistEditorMetadata;
149
+ ownedFrame?: PolishedFrameSplit;
150
+ trustedBaseFrame?: boolean;
151
+ borderColor?: (text: string) => string;
152
+ };
153
+
154
+ type AutocompleteCount = { known: true; count: number } | { known: false };
155
+
156
+ function isStringArray(value: unknown): value is string[] {
157
+ return Array.isArray(value) && value.every((line) => typeof line === "string");
158
+ }
159
+
160
+ function isViewportCounts(value: unknown): value is ViewportCounts {
161
+ if (!value || typeof value !== "object") return false;
162
+ const counts = value as Record<string, unknown>;
163
+ return [counts.above, counts.below].every(
164
+ (count) => count === undefined || (typeof count === "string" && /^[1-9]\d*$/.test(count)),
165
+ );
166
+ }
167
+
168
+ function isPolishedFrameSplit(value: unknown, baseLineCount: number): value is PolishedFrameSplit {
169
+ if (!value || typeof value !== "object") return false;
170
+ const split = value as Record<string, unknown>;
171
+ return (
172
+ isStringArray(split.editorLines) &&
173
+ isStringArray(split.trailingLines) &&
174
+ split.trailingLines.length <= baseLineCount &&
175
+ isViewportCounts(split.viewport)
176
+ );
177
+ }
178
+
179
+ function stripNativeRightPadding(value: string): string {
180
+ return value.replace(/ +$/, "");
181
+ }
182
+
183
+ function autocompleteCount(
184
+ source: AutocompleteEditorInternals,
185
+ capture: AutocompleteCapture | undefined,
186
+ baseRendered: string[],
187
+ ): AutocompleteCount {
188
+ try {
189
+ const showing = source.isShowingAutocomplete;
190
+ if (typeof showing !== "function" || !showing.call(source)) return { known: true, count: 0 };
191
+ if (
192
+ !capture?.compatible ||
193
+ capture.called !== 1 ||
194
+ capture.rows.length <= 0 ||
195
+ capture.rows.length >= baseRendered.length
196
+ )
197
+ return { known: false };
198
+ const suffix = baseRendered.slice(-capture.rows.length);
199
+ if (
200
+ !suffix.every((line, index) => {
201
+ const captured = capture.rows[index];
202
+ return (
203
+ captured !== undefined &&
204
+ (line === captured || stripNativeRightPadding(line) === stripNativeRightPadding(captured))
205
+ );
206
+ })
207
+ )
208
+ return { known: false };
209
+ return { known: true, count: capture.rows.length };
210
+ } catch {
211
+ return { known: false };
212
+ }
213
+ }
214
+
215
+ /** @internal Exported only for descriptor-safety regression tests. */
216
+ export function renderWithAutocompleteCapture<T>(
217
+ source: AutocompleteEditorInternals,
218
+ render: () => T,
219
+ ): { value: T; capture?: AutocompleteCapture } {
220
+ let showing = false;
221
+ try {
222
+ showing =
223
+ typeof source.isShowingAutocomplete === "function" &&
224
+ source.isShowingAutocomplete.call(source);
225
+ } catch {
226
+ return { value: render() };
227
+ }
228
+ if (!showing) return { value: render(), capture: { compatible: true, called: 0, rows: [] } };
229
+
230
+ let list: AutocompleteListInternals;
231
+ let own: PropertyDescriptor | undefined;
232
+ let predecessor: (...args: unknown[]) => unknown;
233
+ try {
234
+ const candidate = source.autocompleteList;
235
+ if (!candidate) return { value: render() };
236
+ own = Object.getOwnPropertyDescriptor(candidate, "render");
237
+ const current = Reflect.get(candidate, "render");
238
+ if (typeof current !== "function") return { value: render() };
239
+ if (own && (!("value" in own) || own.writable !== true)) return { value: render() };
240
+ if (!own && !Object.isExtensible(candidate)) return { value: render() };
241
+ list = candidate;
242
+ predecessor = current as (...args: unknown[]) => unknown;
243
+ } catch {
244
+ return { value: render() };
245
+ }
246
+
247
+ const capture: AutocompleteCapture = { compatible: true, called: 0, rows: [] };
248
+ const wrapper = function (this: AutocompleteListInternals, ...args: unknown[]) {
249
+ const result = Reflect.apply(predecessor, this, args);
250
+ capture.called++;
251
+ if (!isStringArray(result)) {
252
+ capture.compatible = false;
253
+ return result;
254
+ }
255
+ capture.rows = [...result];
256
+ return result;
257
+ };
258
+ const installedDescriptor: PropertyDescriptor = {
259
+ ...(own ?? { configurable: true, enumerable: false, writable: true }),
260
+ value: wrapper,
261
+ };
262
+ try {
263
+ Object.defineProperty(list, "render", installedDescriptor);
264
+ } catch {
265
+ return { value: render() };
266
+ }
267
+
268
+ try {
269
+ return { value: render(), capture };
270
+ } finally {
271
+ let current: PropertyDescriptor | undefined;
272
+ let currentValue: unknown;
273
+ let descriptorKnown = true;
274
+ try {
275
+ current = Object.getOwnPropertyDescriptor(list, "render");
276
+ currentValue = current && "value" in current ? current.value : Reflect.get(list, "render");
277
+ } catch {
278
+ descriptorKnown = false;
279
+ try {
280
+ currentValue = Reflect.get(list, "render");
281
+ } catch {
282
+ currentValue = undefined;
283
+ }
284
+ }
285
+ if (currentValue === wrapper) {
286
+ if (
287
+ !descriptorKnown ||
288
+ !current ||
289
+ current.configurable !== installedDescriptor.configurable ||
290
+ current.enumerable !== installedDescriptor.enumerable ||
291
+ current.writable !== installedDescriptor.writable
292
+ )
293
+ capture.compatible = false;
294
+ try {
295
+ if (own) Object.defineProperty(list, "render", own);
296
+ else if (!Reflect.deleteProperty(list, "render")) capture.compatible = false;
297
+ } catch {
298
+ capture.compatible = false;
299
+ }
300
+ } else {
301
+ // A synchronous third-party replacement wins; captured semantics are no longer trustworthy.
302
+ capture.compatible = false;
303
+ }
304
+ }
305
+ }
306
+
307
+ function clampRenderedLines(lines: string[], width: number): string[] {
308
+ const maxWidth = Math.max(0, width);
309
+ return lines.map((line) => truncateToWidth(line, maxWidth, ""));
310
+ }
311
+
312
+ function fillLine(content: string, width: number): string {
313
+ const truncated = truncateToWidth(content, Math.max(0, width), "");
314
+ const pad = " ".repeat(Math.max(0, width - visibleWidth(truncated)));
315
+ return `${truncated}${pad}`;
316
+ }
317
+
318
+ function isLowRailPolishedStyle(style: EditorStyle): boolean {
319
+ return style === "opencode-copy-friendly";
320
+ }
321
+
322
+ function selectedPolishedConfig(config: ZentuiConfig) {
323
+ switch (config.components.editor.style) {
324
+ case "opencode":
325
+ return config.components.editor.styles.opencode;
326
+ case "opencode-copy-friendly":
327
+ return config.components.editor.styles["opencode-copy-friendly"];
328
+ case "accent-rail":
329
+ case "minimalist":
330
+ return undefined;
331
+ }
332
+ }
333
+
334
+ function lowRailPrompt(config: ZentuiConfig, uiTheme: Theme, reset: string): string {
335
+ const promptIcon = config.icons.editorPrompt;
336
+ return promptIcon
337
+ ? `${renderStyleForSourceOrFallback(
338
+ uiTheme,
339
+ config.components.editor.colorSource,
340
+ config.colors.editorPrompt ?? config.colors.editorAccent,
341
+ EDITOR_ACCENT_FALLBACK,
342
+ promptIcon,
343
+ )}${reset} `
344
+ : "";
345
+ }
346
+
347
+ function getEditorChromeWidths(config: ZentuiConfig, uiTheme: Theme, reset: string) {
348
+ const lowRail = isLowRailPolishedStyle(config.components.editor.style);
349
+ const prompt = lowRailPrompt(config, uiTheme, reset);
350
+ const rail = lowRail
351
+ ? ""
352
+ : `${renderStyleForSourceOrFallback(
353
+ uiTheme,
354
+ config.components.editor.colorSource,
355
+ config.colors.editorAccent,
356
+ EDITOR_ACCENT_FALLBACK,
357
+ config.icons.rail,
358
+ )}${reset} `;
359
+ return {
360
+ prompt,
361
+ promptWidth: visibleWidth(prompt),
362
+ rail,
363
+ railWidth: lowRail ? visibleWidth(prompt) : visibleWidth(rail),
364
+ };
365
+ }
366
+
367
+ export function composeEditorMetadataLine(
368
+ { left, middle, right }: EditorMetadataZones,
369
+ rightStatus: string | undefined,
370
+ width: number,
371
+ ): string {
372
+ const maxWidth = Math.max(0, width);
373
+
374
+ // Preserve the legacy no-fill path exactly, including deferring left-only
375
+ // truncation to the style-specific frame clamp below.
376
+ if (!middle && !right) {
377
+ if (!rightStatus) return left;
378
+ const rightStatusWidth = visibleWidth(rightStatus);
379
+ if (rightStatusWidth >= maxWidth) return truncateToWidth(rightStatus, maxWidth, "");
380
+
381
+ const leftBudget = Math.max(0, maxWidth - rightStatusWidth - 1);
382
+ const leftText = truncateToWidth(left, leftBudget, "");
383
+ const gap = " ".repeat(Math.max(1, maxWidth - visibleWidth(leftText) - rightStatusWidth));
384
+ return `${leftText}${gap}${rightStatus}`;
385
+ }
386
+
387
+ const statusText = rightStatus ? truncateToWidth(rightStatus, maxWidth, "") : "";
388
+ const statusWidth = visibleWidth(statusText);
389
+ const leftBudget = Math.max(0, maxWidth - statusWidth - (statusText && left ? 1 : 0));
390
+ const leftText = truncateToWidth(left, leftBudget, "");
391
+ const leftWidth = visibleWidth(leftText);
392
+
393
+ const rightBudget = Math.max(
394
+ 0,
395
+ maxWidth - leftWidth - statusWidth - (leftText ? 1 : 0) - (statusText ? 1 : 0),
396
+ );
397
+ const configuredRight = truncateToWidth(right, rightBudget, "");
398
+ const rightText =
399
+ configuredRight && statusText
400
+ ? `${configuredRight} ${statusText}`
401
+ : configuredRight || statusText;
402
+ const rightWidth = visibleWidth(rightText);
403
+ const gapWidth = Math.max(0, maxWidth - leftWidth - rightWidth);
404
+ const middleWidth = visibleWidth(middle);
405
+ const minimumMiddleGap = (leftText ? 1 : 0) + (rightText ? 1 : 0);
406
+
407
+ if (!middle || middleWidth + minimumMiddleGap > gapWidth) {
408
+ return `${leftText}${" ".repeat(gapWidth)}${rightText}`;
409
+ }
410
+
411
+ const availablePadding = gapWidth - middleWidth;
412
+ const minimumLeftPadding = leftText ? 1 : 0;
413
+ const maximumLeftPadding = availablePadding - (rightText ? 1 : 0);
414
+ const leftPadding = Math.min(
415
+ maximumLeftPadding,
416
+ Math.max(minimumLeftPadding, Math.floor(availablePadding / 2)),
417
+ );
418
+ return `${leftText}${" ".repeat(leftPadding)}${middle}${" ".repeat(
419
+ availablePadding - leftPadding,
420
+ )}${rightText}`;
421
+ }
422
+
423
+ function ansiStrippedText(line: string): string {
424
+ return line.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
425
+ }
426
+
427
+ function plainRenderedText(line: string): string {
428
+ return ansiStrippedText(line).replace(/\[\/?[^\]]+\]/g, "");
429
+ }
430
+
431
+ function parseEditorBorder(
432
+ line: string,
433
+ direction: keyof ViewportCounts,
434
+ ): { count?: string } | undefined {
435
+ const plain = ansiStrippedText(line);
436
+ if (/^─+$/.test(plain)) return {};
437
+
438
+ const arrow = direction === "above" ? "↑" : "↓";
439
+ const match = new RegExp(`^─── ${arrow} ([1-9]\\d*) more ─*$`).exec(plain);
440
+ return match?.[1] ? { count: match[1] } : undefined;
441
+ }
442
+
443
+ function renderEditorBorder(
444
+ width: number,
445
+ direction: keyof ViewportCounts,
446
+ count: string | undefined,
447
+ ): string {
448
+ if (!count) return "─".repeat(width);
449
+ const arrow = direction === "above" ? "↑" : "↓";
450
+ const indicator = `─── ${arrow} ${count} more `;
451
+ return `${indicator}${"─".repeat(Math.max(0, width - visibleWidth(indicator)))}`;
452
+ }
453
+
454
+ function unwrapPolishedFrameOnly(
455
+ lines: string[],
456
+ config: ZentuiConfig,
457
+ uiTheme: Theme,
458
+ ): { editorLines: string[]; viewport: ViewportCounts } | undefined {
459
+ if (lines.length < 5) return undefined;
460
+ const top = parseEditorBorder(lines[0] ?? "", "above");
461
+ const bottom = parseEditorBorder(lines.at(-1) ?? "", "below");
462
+ if (!top || !bottom) return undefined;
463
+
464
+ const viewport = { above: top.count, below: bottom.count };
465
+ const interior = lines.slice(1, -1);
466
+ if (interior.length < 3) return undefined;
467
+
468
+ if (isLowRailPolishedStyle(config.components.editor.style)) {
469
+ if (
470
+ plainRenderedText(interior[0] ?? "").trim() !== "" ||
471
+ plainRenderedText(interior.at(-2) ?? "").trim() !== "" ||
472
+ !(interior.at(-1) ?? "").startsWith(" ")
473
+ )
474
+ return undefined;
475
+
476
+ const { prompt, promptWidth } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
477
+ const continuation = " ".repeat(promptWidth);
478
+ const content = interior.slice(1, -2);
479
+ const unwrapped: string[] = [];
480
+ for (let index = 0; index < content.length; index++) {
481
+ const prefix = index === 0 ? prompt : continuation;
482
+ const line = content[index] ?? "";
483
+ if (prefix && !line.startsWith(prefix)) return undefined;
484
+ unwrapped.push(prefix ? line.slice(prefix.length) : line);
485
+ }
486
+ return { editorLines: unwrapped, viewport };
487
+ }
488
+
489
+ const { rail } = getEditorChromeWidths(config, uiTheme, "\x1b[0m");
490
+ if (!rail || interior.some((line) => !line.startsWith(rail))) return undefined;
491
+ const unrailed = interior.map((line) => line.slice(rail.length));
492
+ if (
493
+ plainRenderedText(unrailed[0] ?? "").trim() !== "" ||
494
+ plainRenderedText(unrailed.at(-2) ?? "").trim() !== ""
495
+ )
496
+ return undefined;
497
+ return { editorLines: unrailed.slice(1, -2), viewport };
498
+ }
499
+
500
+ function splitPolishedFrame(
501
+ lines: string[],
502
+ config: ZentuiConfig,
503
+ uiTheme: Theme,
504
+ ): PolishedFrameSplit | undefined {
505
+ if (!parseEditorBorder(lines[0] ?? "", "above")) return undefined;
506
+
507
+ for (let bottomIndex = lines.length - 1; bottomIndex >= 4; bottomIndex--) {
508
+ if (!parseEditorBorder(lines[bottomIndex] ?? "", "below")) continue;
509
+ const frame = unwrapPolishedFrameOnly(lines.slice(0, bottomIndex + 1), config, uiTheme);
510
+ if (frame) return { ...frame, trailingLines: lines.slice(bottomIndex + 1) };
511
+ }
512
+ return undefined;
513
+ }
514
+
515
+ function inspectPolishedFrameProvenance(
516
+ base: WrappedEditor,
517
+ rendered: string[],
518
+ config: ZentuiConfig,
519
+ uiTheme: Theme,
520
+ ): { safe: boolean; ownedFrame?: PolishedFrameSplit } {
521
+ const provenance = POLISHED_FRAME_SPLITS.get(rendered);
522
+ const provenanceMatches = Boolean(
523
+ provenance &&
524
+ provenance.rows.length === rendered.length &&
525
+ provenance.rows.every((line, index) => line === rendered[index]),
526
+ );
527
+ const ownedFrame = provenanceMatches ? provenance?.split : undefined;
528
+ const unsafe =
529
+ Boolean(provenance && !provenanceMatches) ||
530
+ LEGACY_SPLIT_POLISHED_FRAME in base ||
531
+ (!ownedFrame && Boolean(splitPolishedFrame(rendered, config, uiTheme)));
532
+ return { safe: !unsafe, ownedFrame };
533
+ }
534
+
535
+ function vimModeColor(mode: string): string {
536
+ switch (mode.toLowerCase()) {
537
+ case "insert":
538
+ return "success";
539
+ case "normal":
540
+ return "accent";
541
+ case "ex":
542
+ return "warning";
543
+ case "replace":
544
+ return "error";
545
+ case "visual":
546
+ return "syntaxKeyword";
547
+ default:
548
+ return "muted";
549
+ }
550
+ }
551
+
552
+ function readVimStatus(editor: WrappedEditor, uiTheme: Theme): string | undefined {
553
+ const mode = editor.getMode?.();
554
+ if (typeof mode !== "string") return undefined;
555
+ const normalized = mode.trim();
556
+ if (!normalized) return undefined;
557
+ const label = `${normalized.toUpperCase()} `;
558
+ return safeThemeFg(uiTheme, vimModeColor(normalized), label);
559
+ }
560
+
561
+ function renderAccentRailFrameFromBase({
562
+ width,
563
+ baseRendered,
564
+ autocompleteSource,
565
+ autocompleteCapture,
566
+ uiTheme,
567
+ config,
568
+ ownedFrame,
569
+ trustedBaseFrame = false,
570
+ }: AccentRailFrameAdapterOptions): PolishedFrameResult {
571
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1 || baseRendered.length < 2) {
572
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
573
+ }
574
+ if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
575
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
576
+ }
577
+ const autocomplete = ownedFrame
578
+ ? { known: true as const, count: ownedFrame.trailingLines.length }
579
+ : autocompleteCount(autocompleteSource, autocompleteCapture, baseRendered);
580
+ if (!autocomplete.known) {
581
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
582
+ }
583
+ const editorFrame =
584
+ !ownedFrame && autocomplete.count > 0
585
+ ? baseRendered.slice(0, -autocomplete.count)
586
+ : baseRendered;
587
+ const autocompleteLines = ownedFrame
588
+ ? ownedFrame.trailingLines
589
+ : autocomplete.count > 0
590
+ ? baseRendered.slice(-autocomplete.count)
591
+ : [];
592
+ if (editorFrame.length < 2) {
593
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
594
+ }
595
+ const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
596
+ const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
597
+ if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
598
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
599
+ }
600
+ const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
601
+ const viewport = ownedFrame?.viewport ?? {
602
+ above: parsedTop?.count,
603
+ below: parsedBottom?.count,
604
+ };
605
+ const lines = renderAccentRailEditorFrame({
606
+ width,
607
+ editorLines,
608
+ autocompleteLines,
609
+ viewport,
610
+ uiTheme,
611
+ config,
612
+ });
613
+ POLISHED_FRAME_SPLITS.set(lines, {
614
+ rows: Object.freeze([...lines]),
615
+ split: { editorLines, trailingLines: autocompleteLines, viewport },
616
+ });
617
+ return { lines, decorated: true };
618
+ }
619
+
620
+ function renderMinimalistFrameFromBase({
621
+ width,
622
+ baseRendered,
623
+ autocompleteSource,
624
+ autocompleteCapture,
625
+ uiTheme,
626
+ config,
627
+ inputText,
628
+ metadata,
629
+ ownedFrame,
630
+ trustedBaseFrame = false,
631
+ borderColor,
632
+ }: MinimalistFrameAdapterOptions): PolishedFrameResult {
633
+ if (width <= 4 || baseRendered.length < 2) {
634
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
635
+ }
636
+ if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
637
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
638
+ }
639
+ const autocomplete = ownedFrame
640
+ ? { known: true as const, count: ownedFrame.trailingLines.length }
641
+ : autocompleteCount(autocompleteSource, autocompleteCapture, baseRendered);
642
+ if (!autocomplete.known) {
643
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
644
+ }
645
+ const editorFrame =
646
+ !ownedFrame && autocomplete.count > 0
647
+ ? baseRendered.slice(0, -autocomplete.count)
648
+ : baseRendered;
649
+ const autocompleteLines = ownedFrame
650
+ ? ownedFrame.trailingLines
651
+ : autocomplete.count > 0
652
+ ? baseRendered.slice(-autocomplete.count)
653
+ : [];
654
+ if (editorFrame.length < 2) {
655
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
656
+ }
657
+ const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
658
+ const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
659
+ if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
660
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
661
+ }
662
+ const viewport = ownedFrame?.viewport ?? {
663
+ above: parsedTop?.count,
664
+ below: parsedBottom?.count,
665
+ };
666
+ return {
667
+ lines: renderMinimalistFrame({
668
+ width,
669
+ editorLines: ownedFrame?.editorLines ?? editorFrame.slice(1, -1),
670
+ autocompleteLines,
671
+ viewport: config.components.editor.viewportIndicators ? viewport : undefined,
672
+ inputText,
673
+ metadata,
674
+ uiTheme,
675
+ config,
676
+ borderColor,
677
+ }),
678
+ decorated: true,
679
+ };
680
+ }
681
+
682
+ function renderPolishedFrame({
683
+ width,
684
+ baseRendered,
685
+ autocompleteSource,
686
+ autocompleteCapture,
687
+ uiTheme,
688
+ config,
689
+ modelMeta,
690
+ thinkingLevel,
691
+ rightStatus,
692
+ ownedFrame,
693
+ trustedBaseFrame = false,
694
+ borderColor,
695
+ }: PolishedFrameOptions): PolishedFrameResult {
696
+ if (width <= 2) return { lines: clampRenderedLines(baseRendered, width), decorated: false };
697
+
698
+ if (baseRendered.length < 2) {
699
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
700
+ }
701
+ if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
702
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
703
+ }
704
+
705
+ const autocomplete = ownedFrame
706
+ ? { known: true, count: ownedFrame.trailingLines.length }
707
+ : autocompleteCount(autocompleteSource, autocompleteCapture, baseRendered);
708
+ if (!autocomplete.known) {
709
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
710
+ }
711
+ const editorFrame =
712
+ !ownedFrame && autocomplete.count > 0
713
+ ? baseRendered.slice(0, -autocomplete.count)
714
+ : baseRendered;
715
+ const autocompleteLines = ownedFrame
716
+ ? ownedFrame.trailingLines
717
+ : autocomplete.count > 0
718
+ ? baseRendered.slice(-autocomplete.count)
719
+ : [];
720
+ if (editorFrame.length < 2) {
721
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
722
+ }
723
+
724
+ const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
725
+ const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
726
+ if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
727
+ return { lines: clampRenderedLines(baseRendered, width), decorated: false };
728
+ }
729
+ const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
730
+ const viewport = ownedFrame?.viewport ?? {
731
+ above: parsedTop?.count,
732
+ below: parsedBottom?.count,
733
+ };
734
+ const lines = renderPolishedEditorFrame({
735
+ width,
736
+ editorLines,
737
+ autocompleteLines,
738
+ viewport,
739
+ uiTheme,
740
+ config,
741
+ modelMeta,
742
+ thinkingLevel,
743
+ rightStatus,
744
+ borderColor,
745
+ });
746
+ POLISHED_FRAME_SPLITS.set(lines, {
747
+ rows: Object.freeze([...lines]),
748
+ split: {
749
+ editorLines,
750
+ trailingLines: autocompleteLines,
751
+ viewport,
752
+ },
753
+ });
754
+ return { lines, decorated: true };
755
+ }
756
+
757
+ /** Pure Opencode frame composition shared by the live editor and settings preview. */
758
+ export function renderPolishedEditorFrame({
759
+ width,
760
+ editorLines,
761
+ autocompleteLines = [],
762
+ viewport = {},
763
+ uiTheme,
764
+ config,
765
+ modelMeta,
766
+ thinkingLevel,
767
+ rightStatus,
768
+ borderColor,
769
+ }: PolishedEditorFrameOptions): string[] {
770
+ if (width <= 2) return clampRenderedLines(editorLines, width);
771
+ const reset = "\x1b[0m";
772
+ const colorSource = config.components.editor.colorSource;
773
+ const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(config, uiTheme, reset);
774
+ const innerWidth = Math.max(0, width - railWidth);
775
+ const lowRailContinuation = " ".repeat(promptWidth);
776
+ const metadataZones = renderEditorMetadataFormatSplit(
777
+ selectedPolishedConfig(config)?.metadataFormat ??
778
+ config.components.editor.styles.opencode.metadataFormat,
779
+ {
780
+ model: modelMeta.modelLabel,
781
+ modelId: modelMeta.modelId ?? "",
782
+ modelName: modelMeta.modelName ?? "",
783
+ provider: modelMeta.providerLabel,
784
+ thinking: thinkingLevel ?? "",
785
+ sessionName: modelMeta.sessionName ?? "",
786
+ contextPercent: modelMeta.contextPercent,
787
+ contextWindow: modelMeta.contextWindow,
788
+ inputTokens: modelMeta.inputTokens,
789
+ outputTokens: modelMeta.outputTokens,
790
+ cacheHitRate: modelMeta.cacheHitRate,
791
+ },
792
+ uiTheme,
793
+ config,
794
+ );
795
+ const lowRailMeta = composeEditorMetadataLine(metadataZones, rightStatus, Math.max(0, width - 1));
796
+ const railedMeta = composeEditorMetadataLine(metadataZones, rightStatus, innerWidth);
797
+
798
+ const renderStaticBorder = (text: string) =>
799
+ renderStyleForSourceOrFallback(
800
+ uiTheme,
801
+ colorSource,
802
+ config.colors.editorBorder,
803
+ EDITOR_BORDER_FALLBACK,
804
+ text,
805
+ );
806
+ const renderBorder = (text: string) => {
807
+ if (
808
+ config.components.editor.borderColorMode !== "adaptive" ||
809
+ typeof borderColor !== "function"
810
+ ) {
811
+ return renderStaticBorder(text);
812
+ }
813
+ try {
814
+ const rendered = borderColor(text);
815
+ return typeof rendered === "string" ? rendered : renderStaticBorder(text);
816
+ } catch {
817
+ return renderStaticBorder(text);
818
+ }
819
+ };
820
+ const top = renderBorder(
821
+ renderEditorBorder(
822
+ width,
823
+ "above",
824
+ config.components.editor.viewportIndicators ? viewport.above : undefined,
825
+ ),
826
+ );
827
+ const bottom = renderBorder(
828
+ renderEditorBorder(
829
+ width,
830
+ "below",
831
+ config.components.editor.viewportIndicators ? viewport.below : undefined,
832
+ ),
833
+ );
834
+ const completionLines =
835
+ selectedPolishedConfig(config)?.completionMenu === "palette"
836
+ ? renderCompletionPalette({
837
+ lines: autocompleteLines,
838
+ width,
839
+ theme: uiTheme,
840
+ renderSeparator: renderBorder,
841
+ ownedBackground: false,
842
+ })
843
+ : autocompleteLines;
844
+ const lines = ["", ...editorLines, "", railedMeta];
845
+ const renderedLines = isLowRailPolishedStyle(config.components.editor.style)
846
+ ? [
847
+ top,
848
+ "",
849
+ ...editorLines.map(
850
+ (line, index) =>
851
+ `${index === 0 ? prompt : lowRailContinuation}${fillLine(line, innerWidth)}`,
852
+ ),
853
+ "",
854
+ ` ${truncateToWidth(lowRailMeta, Math.max(0, width - 1), "")}`,
855
+ bottom,
856
+ ...completionLines,
857
+ ]
858
+ : [
859
+ top,
860
+ ...lines.map((line) => `${rail}${fillLine(line, innerWidth)}`),
861
+ bottom,
862
+ ...completionLines,
863
+ ];
864
+
865
+ return clampRenderedLines(renderedLines, width);
866
+ }
867
+
868
+ export class PolishedEditor extends CustomEditor {
869
+ private readonly getModelMeta: () => EditorMeta;
870
+ private readonly getThinkingLevel: () => string | undefined;
871
+ private readonly getMinimalistMetadata: () => MinimalistEditorMetadata;
872
+ private readonly onMinimalistDecorationChange: (active: boolean) => void;
873
+ private readonly getConfig: () => ZentuiConfig;
874
+ private readonly uiTheme: Theme;
875
+
876
+ constructor(
877
+ tui: TUI,
878
+ theme: EditorTheme,
879
+ keybindings: KeybindingsManager,
880
+ uiTheme: Theme,
881
+ getConfig: () => ZentuiConfig,
882
+ getModelMeta: () => EditorMeta,
883
+ getThinkingLevel: () => string | undefined,
884
+ getMinimalistMetadata: () => MinimalistEditorMetadata = () => ({ cwd: "" }),
885
+ onMinimalistDecorationChange: (active: boolean) => void = () => {},
886
+ ) {
887
+ super(tui, theme, keybindings, { paddingX: 0 });
888
+ this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
889
+ this.uiTheme = uiTheme;
890
+ this.getConfig = getConfig;
891
+ this.getModelMeta = getModelMeta;
892
+ this.getThinkingLevel = getThinkingLevel;
893
+ this.getMinimalistMetadata = getMinimalistMetadata;
894
+ this.onMinimalistDecorationChange = onMinimalistDecorationChange;
895
+ }
896
+
897
+ private reportMinimalistDecoration(active: boolean): void {
898
+ this.onMinimalistDecorationChange(active);
899
+ }
900
+
901
+ render(width: number): string[] {
902
+ const config = this.getConfig();
903
+ if (!config.components.editor.enabled) {
904
+ this.reportMinimalistDecoration(false);
905
+ return clampRenderedLines(super.render(width), width);
906
+ }
907
+ if (config.components.editor.style === "accent-rail") {
908
+ this.reportMinimalistDecoration(false);
909
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
910
+ return clampRenderedLines(super.render(width), width);
911
+ }
912
+ let captured: { value: string[]; capture?: AutocompleteCapture };
913
+ try {
914
+ captured = renderWithAutocompleteCapture(
915
+ this as unknown as AutocompleteEditorInternals,
916
+ () => super.render(width - ACCENT_RAIL_CHROME_WIDTH),
917
+ );
918
+ } catch {
919
+ return clampRenderedLines(super.render(width), width);
920
+ }
921
+ try {
922
+ const result = renderAccentRailFrameFromBase({
923
+ width,
924
+ baseRendered: captured.value,
925
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
926
+ autocompleteCapture: captured.capture,
927
+ uiTheme: this.uiTheme,
928
+ config,
929
+ trustedBaseFrame: true,
930
+ });
931
+ if (result.decorated) return result.lines;
932
+ } catch {
933
+ // Decoration is optional; preserve the completed same-render rows below.
934
+ }
935
+ return clampRenderedLines(captured.value, width);
936
+ }
937
+ if (config.components.editor.style === "minimalist") {
938
+ if (width <= 4) {
939
+ this.reportMinimalistDecoration(false);
940
+ return clampRenderedLines(super.render(width), width);
941
+ }
942
+ let captured: { value: string[]; capture?: AutocompleteCapture };
943
+ try {
944
+ captured = renderWithAutocompleteCapture(
945
+ this as unknown as AutocompleteEditorInternals,
946
+ () => super.render(Math.max(0, width - 4)),
947
+ );
948
+ } catch {
949
+ this.reportMinimalistDecoration(false);
950
+ return clampRenderedLines(super.render(width), width);
951
+ }
952
+ try {
953
+ const result = renderMinimalistFrameFromBase({
954
+ width,
955
+ baseRendered: captured.value,
956
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
957
+ autocompleteCapture: captured.capture,
958
+ uiTheme: this.uiTheme,
959
+ config,
960
+ inputText: this.getText(),
961
+ metadata: this.getMinimalistMetadata(),
962
+ trustedBaseFrame: true,
963
+ borderColor: this.borderColor,
964
+ });
965
+ this.reportMinimalistDecoration(result.decorated);
966
+ return result.lines;
967
+ } catch {
968
+ this.reportMinimalistDecoration(false);
969
+ return clampRenderedLines(captured.value, width);
970
+ }
971
+ }
972
+ this.reportMinimalistDecoration(false);
973
+ if (width <= 2) {
974
+ return clampRenderedLines(super.render(width), width);
975
+ }
976
+
977
+ const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
978
+ const innerWidth = Math.max(0, width - railWidth);
979
+ let captured: { value: string[]; capture?: AutocompleteCapture };
980
+ try {
981
+ captured = renderWithAutocompleteCapture(this as unknown as AutocompleteEditorInternals, () =>
982
+ super.render(innerWidth),
983
+ );
984
+ } catch {
985
+ return clampRenderedLines(super.render(width), width);
986
+ }
987
+ try {
988
+ const result = renderPolishedFrame({
989
+ width,
990
+ baseRendered: captured.value,
991
+ autocompleteSource: this as unknown as AutocompleteEditorInternals,
992
+ autocompleteCapture: captured.capture,
993
+ uiTheme: this.uiTheme,
994
+ config,
995
+ modelMeta: this.getModelMeta(),
996
+ thinkingLevel: this.getThinkingLevel(),
997
+ trustedBaseFrame: true,
998
+ borderColor: this.borderColor,
999
+ });
1000
+ if (result.decorated) return result.lines;
1001
+ } catch {
1002
+ // Decoration is optional; preserve the completed same-render rows below.
1003
+ }
1004
+ return clampRenderedLines(captured.value, width);
1005
+ }
1006
+ }
1007
+
1008
+ export class WrappedPolishedEditor implements EditorComponent {
1009
+ declare readonly addToHistory?: (text: string) => void;
1010
+ declare readonly insertTextAtCursor?: (text: string) => void;
1011
+ declare readonly setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
1012
+ declare readonly setPaddingX?: (padding: number) => void;
1013
+ declare readonly setAutocompleteMaxVisible?: (maxVisible: number) => void;
1014
+
1015
+ constructor(
1016
+ private readonly base: WrappedEditor,
1017
+ private readonly uiTheme: Theme,
1018
+ private readonly getConfig: () => ZentuiConfig,
1019
+ private readonly getModelMeta: () => EditorMeta,
1020
+ private readonly getThinkingLevel: () => string | undefined,
1021
+ private readonly getMinimalistMetadata: () => MinimalistEditorMetadata = () => ({ cwd: "" }),
1022
+ private readonly onMinimalistDecorationChange: (active: boolean) => void = () => {},
1023
+ ) {
1024
+ if (typeof base.addToHistory === "function") {
1025
+ this.addToHistory = (text) => base.addToHistory?.(text);
1026
+ }
1027
+ if (typeof base.insertTextAtCursor === "function") {
1028
+ this.insertTextAtCursor = (text) => base.insertTextAtCursor?.(text);
1029
+ }
1030
+ if (typeof base.setAutocompleteProvider === "function") {
1031
+ this.setAutocompleteProvider = (provider) => base.setAutocompleteProvider?.(provider);
1032
+ }
1033
+ if (typeof base.setPaddingX === "function") {
1034
+ this.setPaddingX = (padding) => base.setPaddingX?.(padding);
1035
+ }
1036
+ if (typeof base.setAutocompleteMaxVisible === "function") {
1037
+ this.setAutocompleteMaxVisible = (maxVisible) => base.setAutocompleteMaxVisible?.(maxVisible);
1038
+ }
1039
+ }
1040
+
1041
+ get focused(): boolean {
1042
+ return Boolean(this.base.focused);
1043
+ }
1044
+ set focused(value: boolean) {
1045
+ this.base.focused = value;
1046
+ }
1047
+
1048
+ get borderColor(): ((str: string) => string) | undefined {
1049
+ return this.base.borderColor;
1050
+ }
1051
+ set borderColor(value: ((str: string) => string) | undefined) {
1052
+ this.base.borderColor = value;
1053
+ }
1054
+
1055
+ get onSubmit(): ((text: string) => void) | undefined {
1056
+ return this.base.onSubmit;
1057
+ }
1058
+ set onSubmit(value: ((text: string) => void) | undefined) {
1059
+ this.base.onSubmit = value;
1060
+ }
1061
+
1062
+ get onChange(): ((text: string) => void) | undefined {
1063
+ return this.base.onChange;
1064
+ }
1065
+ set onChange(value: ((text: string) => void) | undefined) {
1066
+ this.base.onChange = value;
1067
+ }
1068
+
1069
+ get onEscape(): (() => void) | undefined {
1070
+ return this.base.onEscape;
1071
+ }
1072
+ set onEscape(value: (() => void) | undefined) {
1073
+ this.base.onEscape = value;
1074
+ }
1075
+
1076
+ get onCtrlD(): (() => void) | undefined {
1077
+ return this.base.onCtrlD;
1078
+ }
1079
+ set onCtrlD(value: (() => void) | undefined) {
1080
+ this.base.onCtrlD = value;
1081
+ }
1082
+
1083
+ get onPasteImage(): (() => void) | undefined {
1084
+ return this.base.onPasteImage;
1085
+ }
1086
+ set onPasteImage(value: (() => void) | undefined) {
1087
+ this.base.onPasteImage = value;
1088
+ }
1089
+
1090
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
1091
+ return this.base.onExtensionShortcut;
1092
+ }
1093
+ set onExtensionShortcut(value: ((data: string) => boolean) | undefined) {
1094
+ this.base.onExtensionShortcut = value;
1095
+ }
1096
+
1097
+ get actionHandlers(): Map<unknown, () => void> | undefined {
1098
+ return this.base.actionHandlers;
1099
+ }
1100
+ set actionHandlers(value: Map<unknown, () => void> | undefined) {
1101
+ this.base.actionHandlers = value;
1102
+ }
1103
+
1104
+ get wantsKeyRelease(): boolean | undefined {
1105
+ return this.base.wantsKeyRelease;
1106
+ }
1107
+ set wantsKeyRelease(value: boolean | undefined) {
1108
+ this.base.wantsKeyRelease = value;
1109
+ }
1110
+
1111
+ get disableSubmit(): boolean | undefined {
1112
+ return this.base.disableSubmit;
1113
+ }
1114
+ set disableSubmit(value: boolean | undefined) {
1115
+ this.base.disableSubmit = value;
1116
+ }
1117
+
1118
+ private reportMinimalistDecoration(active: boolean): void {
1119
+ this.onMinimalistDecorationChange(active);
1120
+ }
1121
+
1122
+ render(width: number): string[] {
1123
+ const config = this.getConfig();
1124
+ if (!config.components.editor.enabled) {
1125
+ this.reportMinimalistDecoration(false);
1126
+ return clampRenderedLines(this.base.render(width), width);
1127
+ }
1128
+ if (config.components.editor.style === "accent-rail") {
1129
+ this.reportMinimalistDecoration(false);
1130
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
1131
+ return clampRenderedLines(this.base.render(width), width);
1132
+ }
1133
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1134
+ try {
1135
+ captured = renderWithAutocompleteCapture(this.base, () =>
1136
+ this.base.render(width - ACCENT_RAIL_CHROME_WIDTH),
1137
+ );
1138
+ } catch {
1139
+ return clampRenderedLines(this.base.render(width), width);
1140
+ }
1141
+ try {
1142
+ const provenance = inspectPolishedFrameProvenance(
1143
+ this.base,
1144
+ captured.value,
1145
+ config,
1146
+ this.uiTheme,
1147
+ );
1148
+ if (provenance.safe) {
1149
+ const result = renderAccentRailFrameFromBase({
1150
+ width,
1151
+ baseRendered: captured.value,
1152
+ autocompleteSource: this.base,
1153
+ autocompleteCapture: captured.capture,
1154
+ uiTheme: this.uiTheme,
1155
+ config,
1156
+ ownedFrame: provenance.ownedFrame,
1157
+ });
1158
+ if (result.decorated) return result.lines;
1159
+ }
1160
+ } catch {
1161
+ // Decoration is optional; preserve the completed same-render rows below.
1162
+ }
1163
+ return clampRenderedLines(captured.value, width);
1164
+ }
1165
+ if (config.components.editor.style === "minimalist") {
1166
+ if (width <= 4) {
1167
+ this.reportMinimalistDecoration(false);
1168
+ return clampRenderedLines(this.base.render(width), width);
1169
+ }
1170
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1171
+ try {
1172
+ captured = renderWithAutocompleteCapture(this.base, () =>
1173
+ this.base.render(Math.max(0, width - 4)),
1174
+ );
1175
+ } catch {
1176
+ this.reportMinimalistDecoration(false);
1177
+ return clampRenderedLines(this.base.render(width), width);
1178
+ }
1179
+ try {
1180
+ const provenance = inspectPolishedFrameProvenance(
1181
+ this.base,
1182
+ captured.value,
1183
+ config,
1184
+ this.uiTheme,
1185
+ );
1186
+ if (provenance.safe) {
1187
+ const result = renderMinimalistFrameFromBase({
1188
+ width,
1189
+ baseRendered: captured.value,
1190
+ autocompleteSource: this.base,
1191
+ autocompleteCapture: captured.capture,
1192
+ uiTheme: this.uiTheme,
1193
+ config,
1194
+ inputText: this.base.getText(),
1195
+ metadata: this.getMinimalistMetadata(),
1196
+ ownedFrame: provenance.ownedFrame,
1197
+ borderColor: this.borderColor,
1198
+ });
1199
+ if (result.decorated) {
1200
+ this.reportMinimalistDecoration(true);
1201
+ return result.lines;
1202
+ }
1203
+ }
1204
+ } catch {
1205
+ // Decoration is optional; preserve the completed same-render rows below.
1206
+ }
1207
+ this.reportMinimalistDecoration(false);
1208
+ return clampRenderedLines(captured.value, width);
1209
+ }
1210
+ this.reportMinimalistDecoration(false);
1211
+ if (width <= 2) return clampRenderedLines(this.base.render(width), width);
1212
+
1213
+ const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
1214
+ const innerWidth = Math.max(0, width - railWidth);
1215
+ let captured: { value: string[]; capture?: AutocompleteCapture };
1216
+ try {
1217
+ captured = renderWithAutocompleteCapture(this.base, () => this.base.render(innerWidth));
1218
+ } catch {
1219
+ return clampRenderedLines(this.base.render(width), width);
1220
+ }
1221
+ let result: PolishedFrameResult | undefined;
1222
+ try {
1223
+ const provenance = inspectPolishedFrameProvenance(
1224
+ this.base,
1225
+ captured.value,
1226
+ config,
1227
+ this.uiTheme,
1228
+ );
1229
+ if (provenance.safe) {
1230
+ result = renderPolishedFrame({
1231
+ width,
1232
+ baseRendered: captured.value,
1233
+ autocompleteSource: this.base,
1234
+ autocompleteCapture: captured.capture,
1235
+ uiTheme: this.uiTheme,
1236
+ config,
1237
+ modelMeta: this.getModelMeta(),
1238
+ thinkingLevel: this.getThinkingLevel(),
1239
+ rightStatus: readVimStatus(this.base, this.uiTheme),
1240
+ ownedFrame: provenance.ownedFrame,
1241
+ borderColor: this.borderColor,
1242
+ });
1243
+ }
1244
+ } catch {
1245
+ // Decoration is optional; preserve the completed same-render rows below.
1246
+ }
1247
+ if (result?.decorated) return result.lines;
1248
+ return clampRenderedLines(captured.value, width);
1249
+ }
1250
+
1251
+ invalidate(): void {
1252
+ this.base.invalidate?.();
1253
+ }
1254
+
1255
+ handleInput(data: string): void {
1256
+ this.base.handleInput(data);
1257
+ }
1258
+
1259
+ getText(): string {
1260
+ return this.base.getText();
1261
+ }
1262
+
1263
+ setText(text: string): void {
1264
+ this.base.setText(text);
1265
+ }
1266
+
1267
+ getExpandedText(): string {
1268
+ return this.base.getExpandedText?.() ?? this.base.getText();
1269
+ }
1270
+
1271
+ getLines(): string[] {
1272
+ return this.base.getLines?.() ?? this.base.getText().split("\n");
1273
+ }
1274
+
1275
+ getCursor(): unknown {
1276
+ return this.base.getCursor?.();
1277
+ }
1278
+
1279
+ getMode(): unknown {
1280
+ return this.base.getMode?.();
1281
+ }
1282
+
1283
+ getPaddingX(): number | undefined {
1284
+ return this.base.getPaddingX?.();
1285
+ }
1286
+
1287
+ getAutocompleteMaxVisible(): number | undefined {
1288
+ return this.base.getAutocompleteMaxVisible?.();
1289
+ }
1290
+ }