reigncode-app 1.3.2

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 (300) hide show
  1. package/AGENTS.md +30 -0
  2. package/Dockerfile +21 -0
  3. package/README.md +51 -0
  4. package/bunfig.toml +3 -0
  5. package/create-effect-simplification-spec.md +515 -0
  6. package/e2e/AGENTS.md +226 -0
  7. package/e2e/actions.ts +1018 -0
  8. package/e2e/app/home.spec.ts +24 -0
  9. package/e2e/app/navigation.spec.ts +10 -0
  10. package/e2e/app/palette.spec.ts +20 -0
  11. package/e2e/app/server-default.spec.ts +58 -0
  12. package/e2e/app/session.spec.ts +16 -0
  13. package/e2e/app/titlebar-history.spec.ts +120 -0
  14. package/e2e/commands/input-focus.spec.ts +15 -0
  15. package/e2e/commands/panels.spec.ts +33 -0
  16. package/e2e/commands/tab-close.spec.ts +32 -0
  17. package/e2e/files/file-open.spec.ts +31 -0
  18. package/e2e/files/file-tree.spec.ts +56 -0
  19. package/e2e/files/file-viewer.spec.ts +156 -0
  20. package/e2e/fixtures.ts +154 -0
  21. package/e2e/models/model-picker.spec.ts +48 -0
  22. package/e2e/models/models-visibility.spec.ts +61 -0
  23. package/e2e/projects/project-edit.spec.ts +43 -0
  24. package/e2e/projects/projects-close.spec.ts +54 -0
  25. package/e2e/projects/projects-switch.spec.ts +116 -0
  26. package/e2e/projects/workspace-new-session.spec.ts +94 -0
  27. package/e2e/projects/workspaces.spec.ts +375 -0
  28. package/e2e/prompt/context.spec.ts +95 -0
  29. package/e2e/prompt/prompt-async.spec.ts +76 -0
  30. package/e2e/prompt/prompt-drop-file-uri.spec.ts +22 -0
  31. package/e2e/prompt/prompt-drop-file.spec.ts +30 -0
  32. package/e2e/prompt/prompt-history.spec.ts +184 -0
  33. package/e2e/prompt/prompt-mention.spec.ts +26 -0
  34. package/e2e/prompt/prompt-multiline.spec.ts +24 -0
  35. package/e2e/prompt/prompt-shell.spec.ts +62 -0
  36. package/e2e/prompt/prompt-slash-open.spec.ts +22 -0
  37. package/e2e/prompt/prompt-slash-share.spec.ts +64 -0
  38. package/e2e/prompt/prompt-slash-terminal.spec.ts +18 -0
  39. package/e2e/prompt/prompt.spec.ts +55 -0
  40. package/e2e/selectors.ts +75 -0
  41. package/e2e/session/session-child-navigation.spec.ts +37 -0
  42. package/e2e/session/session-composer-dock.spec.ts +530 -0
  43. package/e2e/session/session-model-persistence.spec.ts +359 -0
  44. package/e2e/session/session-review.spec.ts +426 -0
  45. package/e2e/session/session-undo-redo.spec.ts +233 -0
  46. package/e2e/session/session.spec.ts +174 -0
  47. package/e2e/settings/settings-keybinds.spec.ts +389 -0
  48. package/e2e/settings/settings-models.spec.ts +122 -0
  49. package/e2e/settings/settings-providers.spec.ts +136 -0
  50. package/e2e/settings/settings.spec.ts +519 -0
  51. package/e2e/sidebar/sidebar-popover-actions.spec.ts +118 -0
  52. package/e2e/sidebar/sidebar-session-links.spec.ts +30 -0
  53. package/e2e/sidebar/sidebar.spec.ts +40 -0
  54. package/e2e/status/status-popover.spec.ts +94 -0
  55. package/e2e/terminal/terminal-init.spec.ts +28 -0
  56. package/e2e/terminal/terminal-reconnect.spec.ts +46 -0
  57. package/e2e/terminal/terminal-tabs.spec.ts +168 -0
  58. package/e2e/terminal/terminal.spec.ts +18 -0
  59. package/e2e/thinking-level.spec.ts +25 -0
  60. package/e2e/tsconfig.json +9 -0
  61. package/e2e/utils.ts +63 -0
  62. package/happydom.ts +75 -0
  63. package/index.html +23 -0
  64. package/package.json +77 -0
  65. package/playwright.config.ts +45 -0
  66. package/public/_headers +17 -0
  67. package/public/oc-theme-preload.js +35 -0
  68. package/script/e2e-local.ts +180 -0
  69. package/src/addons/serialize.test.ts +319 -0
  70. package/src/addons/serialize.ts +634 -0
  71. package/src/app.tsx +308 -0
  72. package/src/components/debug-bar.tsx +443 -0
  73. package/src/components/dialog-connect-provider.tsx +617 -0
  74. package/src/components/dialog-custom-provider-form.ts +158 -0
  75. package/src/components/dialog-custom-provider.test.ts +80 -0
  76. package/src/components/dialog-custom-provider.tsx +329 -0
  77. package/src/components/dialog-edit-project.tsx +255 -0
  78. package/src/components/dialog-fork.tsx +108 -0
  79. package/src/components/dialog-manage-models.tsx +101 -0
  80. package/src/components/dialog-release-notes.tsx +144 -0
  81. package/src/components/dialog-select-directory.tsx +392 -0
  82. package/src/components/dialog-select-file.tsx +466 -0
  83. package/src/components/dialog-select-mcp.tsx +107 -0
  84. package/src/components/dialog-select-model-unpaid.tsx +137 -0
  85. package/src/components/dialog-select-model.tsx +220 -0
  86. package/src/components/dialog-select-provider.tsx +86 -0
  87. package/src/components/dialog-select-server.tsx +649 -0
  88. package/src/components/dialog-settings.tsx +73 -0
  89. package/src/components/file-tree.test.ts +78 -0
  90. package/src/components/file-tree.tsx +507 -0
  91. package/src/components/link.tsx +26 -0
  92. package/src/components/model-tooltip.tsx +91 -0
  93. package/src/components/prompt-input/attachments.test.ts +44 -0
  94. package/src/components/prompt-input/attachments.ts +201 -0
  95. package/src/components/prompt-input/build-request-parts.test.ts +312 -0
  96. package/src/components/prompt-input/build-request-parts.ts +175 -0
  97. package/src/components/prompt-input/context-items.tsx +88 -0
  98. package/src/components/prompt-input/drag-overlay.tsx +25 -0
  99. package/src/components/prompt-input/editor-dom.test.ts +99 -0
  100. package/src/components/prompt-input/editor-dom.ts +148 -0
  101. package/src/components/prompt-input/files.ts +66 -0
  102. package/src/components/prompt-input/history.test.ts +153 -0
  103. package/src/components/prompt-input/history.ts +256 -0
  104. package/src/components/prompt-input/image-attachments.tsx +58 -0
  105. package/src/components/prompt-input/paste.ts +24 -0
  106. package/src/components/prompt-input/placeholder.test.ts +48 -0
  107. package/src/components/prompt-input/placeholder.ts +15 -0
  108. package/src/components/prompt-input/slash-popover.tsx +141 -0
  109. package/src/components/prompt-input/submit.test.ts +346 -0
  110. package/src/components/prompt-input/submit.ts +579 -0
  111. package/src/components/prompt-input.tsx +1595 -0
  112. package/src/components/server/server-row.tsx +130 -0
  113. package/src/components/session/index.ts +5 -0
  114. package/src/components/session/session-context-breakdown.test.ts +61 -0
  115. package/src/components/session/session-context-breakdown.ts +132 -0
  116. package/src/components/session/session-context-format.ts +20 -0
  117. package/src/components/session/session-context-metrics.test.ts +101 -0
  118. package/src/components/session/session-context-metrics.ts +82 -0
  119. package/src/components/session/session-context-tab.tsx +339 -0
  120. package/src/components/session/session-header.tsx +486 -0
  121. package/src/components/session/session-new-view.tsx +91 -0
  122. package/src/components/session/session-sortable-tab.tsx +70 -0
  123. package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
  124. package/src/components/session-context-usage.tsx +122 -0
  125. package/src/components/settings-general.tsx +585 -0
  126. package/src/components/settings-keybinds.tsx +453 -0
  127. package/src/components/settings-list.tsx +5 -0
  128. package/src/components/settings-models.tsx +137 -0
  129. package/src/components/settings-providers.tsx +251 -0
  130. package/src/components/status-popover.tsx +419 -0
  131. package/src/components/terminal.tsx +653 -0
  132. package/src/components/titlebar-history.test.ts +63 -0
  133. package/src/components/titlebar-history.ts +57 -0
  134. package/src/components/titlebar.tsx +312 -0
  135. package/src/constants/file-picker.ts +89 -0
  136. package/src/context/command-keybind.test.ts +69 -0
  137. package/src/context/command.test.ts +25 -0
  138. package/src/context/command.tsx +437 -0
  139. package/src/context/comments.test.ts +186 -0
  140. package/src/context/comments.tsx +243 -0
  141. package/src/context/file/content-cache.ts +88 -0
  142. package/src/context/file/path.test.ts +360 -0
  143. package/src/context/file/path.ts +151 -0
  144. package/src/context/file/tree-store.ts +170 -0
  145. package/src/context/file/types.ts +41 -0
  146. package/src/context/file/view-cache.ts +146 -0
  147. package/src/context/file/watcher.test.ts +149 -0
  148. package/src/context/file/watcher.ts +53 -0
  149. package/src/context/file-content-eviction-accounting.test.ts +65 -0
  150. package/src/context/file.tsx +280 -0
  151. package/src/context/global-sdk.tsx +232 -0
  152. package/src/context/global-sync/bootstrap.ts +206 -0
  153. package/src/context/global-sync/child-store.test.ts +38 -0
  154. package/src/context/global-sync/child-store.ts +281 -0
  155. package/src/context/global-sync/event-reducer.test.ts +552 -0
  156. package/src/context/global-sync/event-reducer.ts +359 -0
  157. package/src/context/global-sync/eviction.ts +28 -0
  158. package/src/context/global-sync/queue.ts +83 -0
  159. package/src/context/global-sync/session-cache.test.ts +102 -0
  160. package/src/context/global-sync/session-cache.ts +62 -0
  161. package/src/context/global-sync/session-load.ts +25 -0
  162. package/src/context/global-sync/session-prefetch.test.ts +96 -0
  163. package/src/context/global-sync/session-prefetch.ts +100 -0
  164. package/src/context/global-sync/session-trim.test.ts +59 -0
  165. package/src/context/global-sync/session-trim.ts +56 -0
  166. package/src/context/global-sync/types.ts +133 -0
  167. package/src/context/global-sync/utils.ts +25 -0
  168. package/src/context/global-sync.test.ts +122 -0
  169. package/src/context/global-sync.tsx +408 -0
  170. package/src/context/highlights.tsx +233 -0
  171. package/src/context/language.tsx +248 -0
  172. package/src/context/layout-scroll.test.ts +64 -0
  173. package/src/context/layout-scroll.ts +126 -0
  174. package/src/context/layout.test.ts +69 -0
  175. package/src/context/layout.tsx +937 -0
  176. package/src/context/local.tsx +422 -0
  177. package/src/context/model-variant.test.ts +86 -0
  178. package/src/context/model-variant.ts +52 -0
  179. package/src/context/models.tsx +163 -0
  180. package/src/context/notification.tsx +373 -0
  181. package/src/context/permission-auto-respond.test.ts +102 -0
  182. package/src/context/permission-auto-respond.ts +51 -0
  183. package/src/context/permission.tsx +277 -0
  184. package/src/context/platform.tsx +99 -0
  185. package/src/context/prompt.tsx +297 -0
  186. package/src/context/sdk.tsx +49 -0
  187. package/src/context/server.tsx +295 -0
  188. package/src/context/settings.tsx +241 -0
  189. package/src/context/sync-optimistic.test.ts +123 -0
  190. package/src/context/sync.tsx +618 -0
  191. package/src/context/terminal-title.ts +51 -0
  192. package/src/context/terminal.test.ts +82 -0
  193. package/src/context/terminal.tsx +437 -0
  194. package/src/entry.tsx +144 -0
  195. package/src/env.d.ts +18 -0
  196. package/src/hooks/use-providers.ts +44 -0
  197. package/src/i18n/ar.ts +855 -0
  198. package/src/i18n/br.ts +867 -0
  199. package/src/i18n/bs.ts +943 -0
  200. package/src/i18n/da.ts +937 -0
  201. package/src/i18n/de.ts +879 -0
  202. package/src/i18n/en.ts +948 -0
  203. package/src/i18n/es.ts +950 -0
  204. package/src/i18n/fr.ts +878 -0
  205. package/src/i18n/ja.ts +861 -0
  206. package/src/i18n/ko.ts +860 -0
  207. package/src/i18n/no.ts +944 -0
  208. package/src/i18n/parity.test.ts +32 -0
  209. package/src/i18n/pl.ts +865 -0
  210. package/src/i18n/ru.ts +946 -0
  211. package/src/i18n/th.ts +933 -0
  212. package/src/i18n/tr.ts +952 -0
  213. package/src/i18n/zh.ts +930 -0
  214. package/src/i18n/zht.ts +925 -0
  215. package/src/index.css +29 -0
  216. package/src/index.ts +6 -0
  217. package/src/pages/directory-layout.tsx +88 -0
  218. package/src/pages/error.tsx +327 -0
  219. package/src/pages/home.tsx +131 -0
  220. package/src/pages/layout/deep-links.ts +50 -0
  221. package/src/pages/layout/helpers.test.ts +211 -0
  222. package/src/pages/layout/helpers.ts +98 -0
  223. package/src/pages/layout/inline-editor.tsx +126 -0
  224. package/src/pages/layout/sidebar-items.tsx +437 -0
  225. package/src/pages/layout/sidebar-project.tsx +384 -0
  226. package/src/pages/layout/sidebar-shell.tsx +125 -0
  227. package/src/pages/layout/sidebar-workspace.tsx +504 -0
  228. package/src/pages/layout.tsx +2509 -0
  229. package/src/pages/session/composer/index.ts +2 -0
  230. package/src/pages/session/composer/session-composer-region.tsx +255 -0
  231. package/src/pages/session/composer/session-composer-state.test.ts +128 -0
  232. package/src/pages/session/composer/session-composer-state.ts +249 -0
  233. package/src/pages/session/composer/session-followup-dock.tsx +109 -0
  234. package/src/pages/session/composer/session-permission-dock.tsx +74 -0
  235. package/src/pages/session/composer/session-question-dock.tsx +449 -0
  236. package/src/pages/session/composer/session-request-tree.ts +52 -0
  237. package/src/pages/session/composer/session-revert-dock.tsx +99 -0
  238. package/src/pages/session/composer/session-todo-dock.tsx +330 -0
  239. package/src/pages/session/file-tab-scroll.test.ts +40 -0
  240. package/src/pages/session/file-tab-scroll.ts +67 -0
  241. package/src/pages/session/file-tabs.tsx +456 -0
  242. package/src/pages/session/handoff.ts +36 -0
  243. package/src/pages/session/helpers.test.ts +181 -0
  244. package/src/pages/session/helpers.ts +198 -0
  245. package/src/pages/session/message-gesture.test.ts +62 -0
  246. package/src/pages/session/message-gesture.ts +21 -0
  247. package/src/pages/session/message-id-from-hash.ts +6 -0
  248. package/src/pages/session/message-timeline.tsx +1013 -0
  249. package/src/pages/session/review-tab.tsx +170 -0
  250. package/src/pages/session/session-layout.ts +20 -0
  251. package/src/pages/session/session-model-helpers.test.ts +51 -0
  252. package/src/pages/session/session-model-helpers.ts +16 -0
  253. package/src/pages/session/session-side-panel.tsx +453 -0
  254. package/src/pages/session/terminal-label.ts +16 -0
  255. package/src/pages/session/terminal-panel.test.ts +25 -0
  256. package/src/pages/session/terminal-panel.tsx +326 -0
  257. package/src/pages/session/use-session-commands.tsx +495 -0
  258. package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
  259. package/src/pages/session/use-session-hash-scroll.ts +197 -0
  260. package/src/pages/session.tsx +1841 -0
  261. package/src/sst-env.d.ts +12 -0
  262. package/src/testing/model-selection.ts +80 -0
  263. package/src/testing/prompt.ts +56 -0
  264. package/src/testing/session-composer.ts +84 -0
  265. package/src/testing/terminal.ts +118 -0
  266. package/src/theme-preload.test.ts +46 -0
  267. package/src/utils/agent.ts +23 -0
  268. package/src/utils/aim.ts +138 -0
  269. package/src/utils/base64.ts +10 -0
  270. package/src/utils/comment-note.ts +88 -0
  271. package/src/utils/id.ts +99 -0
  272. package/src/utils/notification-click.test.ts +27 -0
  273. package/src/utils/notification-click.ts +13 -0
  274. package/src/utils/persist.test.ts +115 -0
  275. package/src/utils/persist.ts +476 -0
  276. package/src/utils/prompt.test.ts +44 -0
  277. package/src/utils/prompt.ts +203 -0
  278. package/src/utils/runtime-adapters.test.ts +62 -0
  279. package/src/utils/runtime-adapters.ts +39 -0
  280. package/src/utils/same.ts +6 -0
  281. package/src/utils/scoped-cache.test.ts +69 -0
  282. package/src/utils/scoped-cache.ts +104 -0
  283. package/src/utils/server-errors.test.ts +131 -0
  284. package/src/utils/server-errors.ts +80 -0
  285. package/src/utils/server-health.test.ts +123 -0
  286. package/src/utils/server-health.ts +91 -0
  287. package/src/utils/server.ts +22 -0
  288. package/src/utils/solid-dnd.tsx +49 -0
  289. package/src/utils/sound.ts +117 -0
  290. package/src/utils/terminal-writer.test.ts +64 -0
  291. package/src/utils/terminal-writer.ts +65 -0
  292. package/src/utils/time.ts +22 -0
  293. package/src/utils/uuid.test.ts +78 -0
  294. package/src/utils/uuid.ts +12 -0
  295. package/src/utils/worktree.test.ts +46 -0
  296. package/src/utils/worktree.ts +73 -0
  297. package/sst-env.d.ts +10 -0
  298. package/tsconfig.json +26 -0
  299. package/vite.config.ts +15 -0
  300. package/vite.js +26 -0
@@ -0,0 +1,634 @@
1
+ /**
2
+ * SerializeAddon - Serialize terminal buffer contents
3
+ *
4
+ * Port of xterm.js addon-serialize for ghostty-web.
5
+ * Enables serialization of terminal contents to a string that can
6
+ * be written back to restore terminal state.
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * const serializeAddon = new SerializeAddon();
11
+ * term.loadAddon(serializeAddon);
12
+ * const content = serializeAddon.serialize();
13
+ * ```
14
+ */
15
+
16
+ import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web"
17
+
18
+ // ============================================================================
19
+ // Buffer Types (matching ghostty-web internal interfaces)
20
+ // ============================================================================
21
+
22
+ interface IBuffer {
23
+ readonly type: "normal" | "alternate"
24
+ readonly cursorX: number
25
+ readonly cursorY: number
26
+ readonly viewportY: number
27
+ readonly baseY: number
28
+ readonly length: number
29
+ getLine(y: number): IBufferLine | undefined
30
+ getNullCell(): IBufferCell
31
+ }
32
+
33
+ interface IBufferLine {
34
+ readonly length: number
35
+ readonly isWrapped: boolean
36
+ getCell(x: number): IBufferCell | undefined
37
+ translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string
38
+ }
39
+
40
+ interface IBufferCell {
41
+ getChars(): string
42
+ getCode(): number
43
+ getWidth(): number
44
+ getFgColorMode(): number
45
+ getBgColorMode(): number
46
+ getFgColor(): number
47
+ getBgColor(): number
48
+ isBold(): number
49
+ isItalic(): number
50
+ isUnderline(): number
51
+ isStrikethrough(): number
52
+ isBlink(): number
53
+ isInverse(): number
54
+ isInvisible(): number
55
+ isFaint(): number
56
+ isDim(): boolean
57
+ }
58
+
59
+ type TerminalBuffers = {
60
+ active?: IBuffer
61
+ normal?: IBuffer
62
+ alternate?: IBuffer
63
+ }
64
+
65
+ const isRecord = (value: unknown): value is Record<string, unknown> => {
66
+ return typeof value === "object" && value !== null
67
+ }
68
+
69
+ const isBuffer = (value: unknown): value is IBuffer => {
70
+ if (!isRecord(value)) return false
71
+ if (typeof value.length !== "number") return false
72
+ if (typeof value.cursorX !== "number") return false
73
+ if (typeof value.cursorY !== "number") return false
74
+ if (typeof value.baseY !== "number") return false
75
+ if (typeof value.viewportY !== "number") return false
76
+ if (typeof value.getLine !== "function") return false
77
+ if (typeof value.getNullCell !== "function") return false
78
+ return true
79
+ }
80
+
81
+ const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined => {
82
+ if (!isRecord(value)) return
83
+ const raw = value.buffer
84
+ if (!isRecord(raw)) return
85
+ const active = isBuffer(raw.active) ? raw.active : undefined
86
+ const normal = isBuffer(raw.normal) ? raw.normal : undefined
87
+ const alternate = isBuffer(raw.alternate) ? raw.alternate : undefined
88
+ if (!active && !normal) return
89
+ return { active, normal, alternate }
90
+ }
91
+
92
+ // ============================================================================
93
+ // Types
94
+ // ============================================================================
95
+
96
+ export interface ISerializeOptions {
97
+ /**
98
+ * The row range to serialize. When an explicit range is specified, the cursor
99
+ * will get its final repositioning.
100
+ */
101
+ range?: ISerializeRange
102
+ /**
103
+ * The number of rows in the scrollback buffer to serialize, starting from
104
+ * the bottom of the scrollback buffer. When not specified, all available
105
+ * rows in the scrollback buffer will be serialized.
106
+ */
107
+ scrollback?: number
108
+ /**
109
+ * Whether to exclude the terminal modes from the serialization.
110
+ * Default: false
111
+ */
112
+ excludeModes?: boolean
113
+ /**
114
+ * Whether to exclude the alt buffer from the serialization.
115
+ * Default: false
116
+ */
117
+ excludeAltBuffer?: boolean
118
+ }
119
+
120
+ export interface ISerializeRange {
121
+ /**
122
+ * The line to start serializing (inclusive).
123
+ */
124
+ start: number
125
+ /**
126
+ * The line to end serializing (inclusive).
127
+ */
128
+ end: number
129
+ }
130
+
131
+ export interface IHTMLSerializeOptions {
132
+ /**
133
+ * The number of rows in the scrollback buffer to serialize, starting from
134
+ * the bottom of the scrollback buffer.
135
+ */
136
+ scrollback?: number
137
+ /**
138
+ * Whether to only serialize the selection.
139
+ * Default: false
140
+ */
141
+ onlySelection?: boolean
142
+ /**
143
+ * Whether to include the global background of the terminal.
144
+ * Default: false
145
+ */
146
+ includeGlobalBackground?: boolean
147
+ /**
148
+ * The range to serialize. This is prioritized over onlySelection.
149
+ */
150
+ range?: {
151
+ startLine: number
152
+ endLine: number
153
+ startCol: number
154
+ }
155
+ }
156
+
157
+ // ============================================================================
158
+ // Helper Functions
159
+ // ============================================================================
160
+
161
+ function constrain(value: number, low: number, high: number): number {
162
+ return Math.max(low, Math.min(value, high))
163
+ }
164
+
165
+ function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
166
+ return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor()
167
+ }
168
+
169
+ function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
170
+ return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor()
171
+ }
172
+
173
+ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
174
+ return (
175
+ !!cell1.isInverse() === !!cell2.isInverse() &&
176
+ !!cell1.isBold() === !!cell2.isBold() &&
177
+ !!cell1.isUnderline() === !!cell2.isUnderline() &&
178
+ !!cell1.isBlink() === !!cell2.isBlink() &&
179
+ !!cell1.isInvisible() === !!cell2.isInvisible() &&
180
+ !!cell1.isItalic() === !!cell2.isItalic() &&
181
+ !!cell1.isDim() === !!cell2.isDim() &&
182
+ !!cell1.isStrikethrough() === !!cell2.isStrikethrough()
183
+ )
184
+ }
185
+
186
+ // ============================================================================
187
+ // Base Serialize Handler
188
+ // ============================================================================
189
+
190
+ abstract class BaseSerializeHandler {
191
+ constructor(protected readonly _buffer: IBuffer) {}
192
+
193
+ public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
194
+ let oldCell = this._buffer.getNullCell()
195
+
196
+ const startRow = range.start.y
197
+ const endRow = range.end.y
198
+ const startColumn = range.start.x
199
+ const endColumn = range.end.x
200
+
201
+ this._beforeSerialize(endRow - startRow + 1, startRow, endRow)
202
+
203
+ for (let row = startRow; row <= endRow; row++) {
204
+ const line = this._buffer.getLine(row)
205
+ if (line) {
206
+ const startLineColumn = row === range.start.y ? startColumn : 0
207
+ const endLineColumn = Math.min(endColumn, line.length)
208
+
209
+ for (let col = startLineColumn; col < endLineColumn; col++) {
210
+ const c = line.getCell(col)
211
+ if (!c) {
212
+ continue
213
+ }
214
+ this._nextCell(c, oldCell, row, col)
215
+ oldCell = c
216
+ }
217
+ }
218
+ this._rowEnd(row, row === endRow)
219
+ }
220
+
221
+ this._afterSerialize()
222
+
223
+ return this._serializeString(excludeFinalCursorPosition)
224
+ }
225
+
226
+ protected _nextCell(_cell: IBufferCell, _oldCell: IBufferCell, _row: number, _col: number): void {}
227
+ protected _rowEnd(_row: number, _isLastRow: boolean): void {}
228
+ protected _beforeSerialize(_rows: number, _startRow: number, _endRow: number): void {}
229
+ protected _afterSerialize(): void {}
230
+ protected _serializeString(_excludeFinalCursorPosition?: boolean): string {
231
+ return ""
232
+ }
233
+ }
234
+
235
+ // ============================================================================
236
+ // String Serialize Handler
237
+ // ============================================================================
238
+
239
+ class StringSerializeHandler extends BaseSerializeHandler {
240
+ private _rowIndex: number = 0
241
+ private _allRows: string[] = []
242
+ private _allRowSeparators: string[] = []
243
+ private _currentRow: string = ""
244
+ private _nullCellCount: number = 0
245
+ private _cursorStyle: IBufferCell
246
+ private _firstRow: number = 0
247
+ private _lastCursorRow: number = 0
248
+ private _lastCursorCol: number = 0
249
+ private _lastContentCursorRow: number = 0
250
+ private _lastContentCursorCol: number = 0
251
+
252
+ constructor(
253
+ buffer: IBuffer,
254
+ private readonly _terminal: ITerminalCore,
255
+ ) {
256
+ super(buffer)
257
+ this._cursorStyle = this._buffer.getNullCell()
258
+ }
259
+
260
+ protected _beforeSerialize(rows: number, start: number, _end: number): void {
261
+ this._allRows = new Array<string>(rows)
262
+ this._allRowSeparators = new Array<string>(rows)
263
+ this._rowIndex = 0
264
+
265
+ this._currentRow = ""
266
+ this._nullCellCount = 0
267
+ this._cursorStyle = this._buffer.getNullCell()
268
+
269
+ this._lastContentCursorRow = start
270
+ this._lastCursorRow = start
271
+ this._firstRow = start
272
+ }
273
+
274
+ protected _rowEnd(row: number, isLastRow: boolean): void {
275
+ let rowSeparator = ""
276
+
277
+ const nextLine = isLastRow ? undefined : this._buffer.getLine(row + 1)
278
+ const wrapped = !!nextLine?.isWrapped
279
+
280
+ if (this._nullCellCount > 0 && wrapped) {
281
+ this._currentRow += " ".repeat(this._nullCellCount)
282
+ }
283
+
284
+ this._nullCellCount = 0
285
+
286
+ if (!isLastRow && !wrapped) {
287
+ rowSeparator = "\r\n"
288
+ this._lastCursorRow = row + 1
289
+ this._lastCursorCol = 0
290
+ }
291
+
292
+ this._allRows[this._rowIndex] = this._currentRow
293
+ this._allRowSeparators[this._rowIndex++] = rowSeparator
294
+ this._currentRow = ""
295
+ this._nullCellCount = 0
296
+ }
297
+
298
+ private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
299
+ const sgrSeq: number[] = []
300
+ const fgChanged = !equalFg(cell, oldCell)
301
+ const bgChanged = !equalBg(cell, oldCell)
302
+ const flagsChanged = !equalFlags(cell, oldCell)
303
+
304
+ if (fgChanged || bgChanged || flagsChanged) {
305
+ if (this._isAttributeDefault(cell)) {
306
+ if (!this._isAttributeDefault(oldCell)) {
307
+ sgrSeq.push(0)
308
+ }
309
+ } else {
310
+ if (flagsChanged) {
311
+ if (!!cell.isInverse() !== !!oldCell.isInverse()) {
312
+ sgrSeq.push(cell.isInverse() ? 7 : 27)
313
+ }
314
+ if (!!cell.isBold() !== !!oldCell.isBold()) {
315
+ sgrSeq.push(cell.isBold() ? 1 : 22)
316
+ }
317
+ if (!!cell.isUnderline() !== !!oldCell.isUnderline()) {
318
+ sgrSeq.push(cell.isUnderline() ? 4 : 24)
319
+ }
320
+ if (!!cell.isBlink() !== !!oldCell.isBlink()) {
321
+ sgrSeq.push(cell.isBlink() ? 5 : 25)
322
+ }
323
+ if (!!cell.isInvisible() !== !!oldCell.isInvisible()) {
324
+ sgrSeq.push(cell.isInvisible() ? 8 : 28)
325
+ }
326
+ if (!!cell.isItalic() !== !!oldCell.isItalic()) {
327
+ sgrSeq.push(cell.isItalic() ? 3 : 23)
328
+ }
329
+ if (!!cell.isDim() !== !!oldCell.isDim()) {
330
+ sgrSeq.push(cell.isDim() ? 2 : 22)
331
+ }
332
+ if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) {
333
+ sgrSeq.push(cell.isStrikethrough() ? 9 : 29)
334
+ }
335
+ }
336
+ if (fgChanged) {
337
+ const color = cell.getFgColor()
338
+ const mode = cell.getFgColorMode()
339
+ if (mode === 2 || mode === 3 || mode === -1) {
340
+ sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
341
+ } else if (mode === 1) {
342
+ // Palette
343
+ if (color >= 16) {
344
+ sgrSeq.push(38, 5, color)
345
+ } else {
346
+ sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7))
347
+ }
348
+ } else {
349
+ sgrSeq.push(39)
350
+ }
351
+ }
352
+ if (bgChanged) {
353
+ const color = cell.getBgColor()
354
+ const mode = cell.getBgColorMode()
355
+ if (mode === 2 || mode === 3 || mode === -1) {
356
+ sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
357
+ } else if (mode === 1) {
358
+ // Palette
359
+ if (color >= 16) {
360
+ sgrSeq.push(48, 5, color)
361
+ } else {
362
+ sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7))
363
+ }
364
+ } else {
365
+ sgrSeq.push(49)
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ return sgrSeq
372
+ }
373
+
374
+ private _isAttributeDefault(cell: IBufferCell): boolean {
375
+ const mode = cell.getFgColorMode()
376
+ const bgMode = cell.getBgColorMode()
377
+
378
+ if (mode === 0 && bgMode === 0) {
379
+ return (
380
+ !cell.isBold() &&
381
+ !cell.isItalic() &&
382
+ !cell.isUnderline() &&
383
+ !cell.isBlink() &&
384
+ !cell.isInverse() &&
385
+ !cell.isInvisible() &&
386
+ !cell.isDim() &&
387
+ !cell.isStrikethrough()
388
+ )
389
+ }
390
+
391
+ const fgColor = cell.getFgColor()
392
+ const bgColor = cell.getBgColor()
393
+ const nullCell = this._buffer.getNullCell()
394
+ const nullFg = nullCell.getFgColor()
395
+ const nullBg = nullCell.getBgColor()
396
+
397
+ return (
398
+ fgColor === nullFg &&
399
+ bgColor === nullBg &&
400
+ !cell.isBold() &&
401
+ !cell.isItalic() &&
402
+ !cell.isUnderline() &&
403
+ !cell.isBlink() &&
404
+ !cell.isInverse() &&
405
+ !cell.isInvisible() &&
406
+ !cell.isDim() &&
407
+ !cell.isStrikethrough()
408
+ )
409
+ }
410
+
411
+ protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void {
412
+ const isPlaceHolderCell = cell.getWidth() === 0
413
+
414
+ if (isPlaceHolderCell) {
415
+ return
416
+ }
417
+
418
+ const codepoint = cell.getCode()
419
+ const isInvalidCodepoint = codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)
420
+ const isGarbage = isInvalidCodepoint || (codepoint >= 0xf000 && cell.getWidth() === 1)
421
+ const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
422
+
423
+ const sgrSeq = this._diffStyle(cell, this._cursorStyle)
424
+
425
+ const styleChanged = sgrSeq.length > 0
426
+
427
+ if (styleChanged) {
428
+ if (this._nullCellCount > 0) {
429
+ this._currentRow += " ".repeat(this._nullCellCount)
430
+ this._nullCellCount = 0
431
+ }
432
+
433
+ this._lastContentCursorRow = this._lastCursorRow = row
434
+ this._lastContentCursorCol = this._lastCursorCol = col
435
+
436
+ this._currentRow += `\u001b[${sgrSeq.join(";")}m`
437
+
438
+ const line = this._buffer.getLine(row)
439
+ const cellFromLine = line?.getCell(col)
440
+ if (cellFromLine) {
441
+ this._cursorStyle = cellFromLine
442
+ }
443
+ }
444
+
445
+ if (isEmptyCell) {
446
+ this._nullCellCount += cell.getWidth()
447
+ } else {
448
+ if (this._nullCellCount > 0) {
449
+ this._currentRow += " ".repeat(this._nullCellCount)
450
+ this._nullCellCount = 0
451
+ }
452
+
453
+ this._currentRow += cell.getChars()
454
+
455
+ this._lastContentCursorRow = this._lastCursorRow = row
456
+ this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth()
457
+ }
458
+ }
459
+
460
+ protected _serializeString(excludeFinalCursorPosition?: boolean): string {
461
+ let rowEnd = this._allRows.length
462
+
463
+ if (this._buffer.length - this._firstRow <= this._terminal.rows) {
464
+ rowEnd = this._lastContentCursorRow + 1 - this._firstRow
465
+ this._lastCursorCol = this._lastContentCursorCol
466
+ this._lastCursorRow = this._lastContentCursorRow
467
+ }
468
+
469
+ let content = ""
470
+
471
+ for (let i = 0; i < rowEnd; i++) {
472
+ content += this._allRows[i]
473
+ if (i + 1 < rowEnd) {
474
+ content += this._allRowSeparators[i]
475
+ }
476
+ }
477
+
478
+ if (excludeFinalCursorPosition) return content
479
+
480
+ const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
481
+ const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
482
+ const cursorCol = this._buffer.cursorX + 1
483
+ content += `\u001b[${cursorRow};${cursorCol}H`
484
+
485
+ const line = this._buffer.getLine(absoluteCursorRow)
486
+ const cell = line?.getCell(this._buffer.cursorX)
487
+ const style = (() => {
488
+ if (!cell) return this._buffer.getNullCell()
489
+ if (cell.getWidth() !== 0) return cell
490
+ if (this._buffer.cursorX > 0) return line?.getCell(this._buffer.cursorX - 1) ?? cell
491
+ return cell
492
+ })()
493
+
494
+ const sgrSeq = this._diffStyle(style, this._cursorStyle)
495
+ if (sgrSeq.length) content += `\u001b[${sgrSeq.join(";")}m`
496
+
497
+ return content
498
+ }
499
+ }
500
+
501
+ // ============================================================================
502
+ // SerializeAddon Class
503
+ // ============================================================================
504
+
505
+ export class SerializeAddon implements ITerminalAddon {
506
+ private _terminal?: ITerminalCore
507
+
508
+ /**
509
+ * Activate the addon (called by Terminal.loadAddon)
510
+ */
511
+ public activate(terminal: ITerminalCore): void {
512
+ this._terminal = terminal
513
+ }
514
+
515
+ /**
516
+ * Dispose the addon and clean up resources
517
+ */
518
+ public dispose(): void {
519
+ this._terminal = undefined
520
+ }
521
+
522
+ /**
523
+ * Serializes terminal rows into a string that can be written back to the
524
+ * terminal to restore the state. The cursor will also be positioned to the
525
+ * correct cell.
526
+ *
527
+ * @param options Custom options to allow control over what gets serialized.
528
+ */
529
+ public serialize(options?: ISerializeOptions): string {
530
+ if (!this._terminal) {
531
+ throw new Error("Cannot use addon until it has been loaded")
532
+ }
533
+
534
+ const buffer = getTerminalBuffers(this._terminal)
535
+
536
+ if (!buffer) {
537
+ return ""
538
+ }
539
+
540
+ const normalBuffer = buffer.normal ?? buffer.active
541
+ const altBuffer = buffer.alternate
542
+
543
+ if (!normalBuffer) {
544
+ return ""
545
+ }
546
+
547
+ let content = options?.range
548
+ ? this._serializeBufferByRange(normalBuffer, options.range, true)
549
+ : this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
550
+
551
+ if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) {
552
+ const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
553
+ content += `\u001b[?1049h\u001b[H${alternateContent}`
554
+ }
555
+
556
+ return content
557
+ }
558
+
559
+ /**
560
+ * Serializes terminal content as plain text (no escape sequences)
561
+ * @param options Custom options to allow control over what gets serialized.
562
+ */
563
+ public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string {
564
+ if (!this._terminal) {
565
+ throw new Error("Cannot use addon until it has been loaded")
566
+ }
567
+
568
+ const buffer = getTerminalBuffers(this._terminal)
569
+
570
+ if (!buffer) {
571
+ return ""
572
+ }
573
+
574
+ const activeBuffer = buffer.active ?? buffer.normal
575
+ if (!activeBuffer) {
576
+ return ""
577
+ }
578
+
579
+ const maxRows = activeBuffer.length
580
+ const scrollback = options?.scrollback
581
+ const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows)
582
+
583
+ const startRow = maxRows - correctRows
584
+ const endRow = maxRows - 1
585
+ const lines: string[] = []
586
+
587
+ for (let row = startRow; row <= endRow; row++) {
588
+ const line = activeBuffer.getLine(row)
589
+ if (line) {
590
+ const text = line.translateToString(options?.trimWhitespace ?? true)
591
+ lines.push(text)
592
+ }
593
+ }
594
+
595
+ // Trim trailing empty lines if requested
596
+ if (options?.trimWhitespace) {
597
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
598
+ lines.pop()
599
+ }
600
+ }
601
+
602
+ return lines.join("\n")
603
+ }
604
+
605
+ private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string {
606
+ const maxRows = buffer.length
607
+ const rows = this._terminal?.rows ?? 24
608
+ const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows)
609
+ return this._serializeBufferByRange(
610
+ buffer,
611
+ {
612
+ start: maxRows - correctRows,
613
+ end: maxRows - 1,
614
+ },
615
+ false,
616
+ )
617
+ }
618
+
619
+ private _serializeBufferByRange(
620
+ buffer: IBuffer,
621
+ range: ISerializeRange,
622
+ excludeFinalCursorPosition: boolean,
623
+ ): string {
624
+ const handler = new StringSerializeHandler(buffer, this._terminal!)
625
+ const cols = this._terminal?.cols ?? 80
626
+ return handler.serialize(
627
+ {
628
+ start: { x: 0, y: range.start },
629
+ end: { x: cols, y: range.end },
630
+ },
631
+ excludeFinalCursorPosition,
632
+ )
633
+ }
634
+ }