@jonsoc/app 1.1.34

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