@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,319 @@
1
+ import { describe, test, expect, beforeAll, afterEach } from "bun:test"
2
+ import { Terminal, Ghostty } from "ghostty-web"
3
+ import { SerializeAddon } from "./serialize"
4
+
5
+ let ghostty: Ghostty
6
+ beforeAll(async () => {
7
+ ghostty = await Ghostty.load()
8
+ })
9
+
10
+ const terminals: Terminal[] = []
11
+
12
+ afterEach(() => {
13
+ for (const term of terminals) {
14
+ term.dispose()
15
+ }
16
+ terminals.length = 0
17
+ document.body.innerHTML = ""
18
+ })
19
+
20
+ function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } {
21
+ const container = document.createElement("div")
22
+ document.body.appendChild(container)
23
+
24
+ const term = new Terminal({ cols, rows, ghostty })
25
+ const addon = new SerializeAddon()
26
+ term.loadAddon(addon)
27
+ term.open(container)
28
+ terminals.push(term)
29
+
30
+ return { term, addon, container }
31
+ }
32
+
33
+ function writeAndWait(term: Terminal, data: string): Promise<void> {
34
+ return new Promise((resolve) => {
35
+ term.write(data, resolve)
36
+ })
37
+ }
38
+
39
+ describe("SerializeAddon", () => {
40
+ describe("ANSI color preservation", () => {
41
+ test("should preserve text attributes (bold, italic, underline)", async () => {
42
+ const { term, addon } = createTerminal()
43
+
44
+ const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m"
45
+ await writeAndWait(term, input)
46
+
47
+ const origLine = term.buffer.active.getLine(0)
48
+ expect(origLine!.getCell(0)!.isBold()).toBe(1)
49
+ expect(origLine!.getCell(5)!.isItalic()).toBe(1)
50
+ expect(origLine!.getCell(12)!.isUnderline()).toBe(1)
51
+
52
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
53
+
54
+ const { term: term2 } = createTerminal()
55
+ terminals.push(term2)
56
+ await writeAndWait(term2, serialized)
57
+
58
+ const line = term2.buffer.active.getLine(0)
59
+
60
+ const boldCell = line!.getCell(0)
61
+ expect(boldCell!.getChars()).toBe("B")
62
+ expect(boldCell!.isBold()).toBe(1)
63
+
64
+ const italicCell = line!.getCell(5)
65
+ expect(italicCell!.getChars()).toBe("I")
66
+ expect(italicCell!.isItalic()).toBe(1)
67
+
68
+ const underCell = line!.getCell(12)
69
+ expect(underCell!.getChars()).toBe("U")
70
+ expect(underCell!.isUnderline()).toBe(1)
71
+ })
72
+
73
+ test("should preserve basic 16-color foreground colors", async () => {
74
+ const { term, addon } = createTerminal()
75
+
76
+ const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL"
77
+ await writeAndWait(term, input)
78
+
79
+ const origLine = term.buffer.active.getLine(0)
80
+ const origRedFg = origLine!.getCell(0)!.getFgColor()
81
+ const origGreenFg = origLine!.getCell(3)!.getFgColor()
82
+ const origBlueFg = origLine!.getCell(8)!.getFgColor()
83
+
84
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
85
+
86
+ const { term: term2 } = createTerminal()
87
+ terminals.push(term2)
88
+ await writeAndWait(term2, serialized)
89
+
90
+ const line = term2.buffer.active.getLine(0)
91
+ expect(line).toBeDefined()
92
+
93
+ const redCell = line!.getCell(0)
94
+ expect(redCell!.getChars()).toBe("R")
95
+ expect(redCell!.getFgColor()).toBe(origRedFg)
96
+
97
+ const greenCell = line!.getCell(3)
98
+ expect(greenCell!.getChars()).toBe("G")
99
+ expect(greenCell!.getFgColor()).toBe(origGreenFg)
100
+
101
+ const blueCell = line!.getCell(8)
102
+ expect(blueCell!.getChars()).toBe("B")
103
+ expect(blueCell!.getFgColor()).toBe(origBlueFg)
104
+ })
105
+
106
+ test("should preserve 256-color palette colors", async () => {
107
+ const { term, addon } = createTerminal()
108
+
109
+ const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL"
110
+ await writeAndWait(term, input)
111
+
112
+ const origLine = term.buffer.active.getLine(0)
113
+ const origRedFg = origLine!.getCell(0)!.getFgColor()
114
+
115
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
116
+
117
+ const { term: term2 } = createTerminal()
118
+ terminals.push(term2)
119
+ await writeAndWait(term2, serialized)
120
+
121
+ const line = term2.buffer.active.getLine(0)
122
+ const redCell = line!.getCell(0)
123
+ expect(redCell!.getChars()).toBe("R")
124
+ expect(redCell!.getFgColor()).toBe(origRedFg)
125
+ })
126
+
127
+ test("should preserve RGB/truecolor colors", async () => {
128
+ const { term, addon } = createTerminal()
129
+
130
+ const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL"
131
+ await writeAndWait(term, input)
132
+
133
+ const origLine = term.buffer.active.getLine(0)
134
+ const origRgbFg = origLine!.getCell(0)!.getFgColor()
135
+
136
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
137
+
138
+ const { term: term2 } = createTerminal()
139
+ terminals.push(term2)
140
+ await writeAndWait(term2, serialized)
141
+
142
+ const line = term2.buffer.active.getLine(0)
143
+ const rgbCell = line!.getCell(0)
144
+ expect(rgbCell!.getChars()).toBe("R")
145
+ expect(rgbCell!.getFgColor()).toBe(origRgbFg)
146
+ })
147
+
148
+ test("should preserve background colors", async () => {
149
+ const { term, addon } = createTerminal()
150
+
151
+ const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL"
152
+ await writeAndWait(term, input)
153
+
154
+ const origLine = term.buffer.active.getLine(0)
155
+ const origRedBg = origLine!.getCell(0)!.getBgColor()
156
+ const origGreenBg = origLine!.getCell(6)!.getBgColor()
157
+
158
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
159
+
160
+ const { term: term2 } = createTerminal()
161
+ terminals.push(term2)
162
+ await writeAndWait(term2, serialized)
163
+
164
+ const line = term2.buffer.active.getLine(0)
165
+
166
+ const redBgCell = line!.getCell(0)
167
+ expect(redBgCell!.getChars()).toBe("R")
168
+ expect(redBgCell!.getBgColor()).toBe(origRedBg)
169
+
170
+ const greenBgCell = line!.getCell(6)
171
+ expect(greenBgCell!.getChars()).toBe("G")
172
+ expect(greenBgCell!.getBgColor()).toBe(origGreenBg)
173
+ })
174
+
175
+ test("should handle combined colors and attributes", async () => {
176
+ const { term, addon } = createTerminal()
177
+
178
+ const input =
179
+ "\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL "
180
+ await writeAndWait(term, input)
181
+
182
+ const origLine = term.buffer.active.getLine(0)
183
+ const origFg = origLine!.getCell(0)!.getFgColor()
184
+ const origBg = origLine!.getCell(0)!.getBgColor()
185
+ expect(origLine!.getCell(0)!.isBold()).toBe(1)
186
+
187
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
188
+ const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "")
189
+
190
+ expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true)
191
+
192
+ const { term: term2 } = createTerminal()
193
+ terminals.push(term2)
194
+ await writeAndWait(term2, cleanSerialized)
195
+
196
+ const line = term2.buffer.active.getLine(0)
197
+ const comboCell = line!.getCell(0)
198
+
199
+ expect(comboCell!.getChars()).toBe("C")
200
+ expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m")
201
+ })
202
+ })
203
+
204
+ describe("round-trip serialization", () => {
205
+ test("should not produce ECH sequences", async () => {
206
+ const { term, addon } = createTerminal()
207
+
208
+ await writeAndWait(term, "\x1b[31mHello\x1b[0m World")
209
+
210
+ const serialized = addon.serialize()
211
+
212
+ const hasECH = /\x1b\[\d+X/.test(serialized)
213
+ expect(hasECH).toBe(false)
214
+ })
215
+
216
+ test("multi-line content should not have garbage characters", async () => {
217
+ const { term, addon } = createTerminal()
218
+
219
+ const content = [
220
+ "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path",
221
+ "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la",
222
+ "total 42",
223
+ ].join("\r\n")
224
+
225
+ await writeAndWait(term, content)
226
+
227
+ const serialized = addon.serialize()
228
+
229
+ expect(/\x1b\[\d+X/.test(serialized)).toBe(false)
230
+
231
+ const { term: term2 } = createTerminal()
232
+ terminals.push(term2)
233
+ await writeAndWait(term2, serialized)
234
+
235
+ for (let row = 0; row < 3; row++) {
236
+ const line = term2.buffer.active.getLine(row)?.translateToString(true)
237
+ expect(line?.includes("𑼝")).toBe(false)
238
+ }
239
+
240
+ expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
241
+ expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
242
+ expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
243
+ })
244
+
245
+ test("serialized output should restore after Terminal.reset()", async () => {
246
+ const { term, addon } = createTerminal()
247
+
248
+ const content = [
249
+ "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path",
250
+ "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la",
251
+ "total 42",
252
+ ].join("\r\n")
253
+
254
+ await writeAndWait(term, content)
255
+
256
+ const serialized = addon.serialize()
257
+
258
+ const { term: term2 } = createTerminal()
259
+ terminals.push(term2)
260
+ term2.reset()
261
+ await writeAndWait(term2, serialized)
262
+
263
+ expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
264
+ expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
265
+ expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
266
+ })
267
+
268
+ test("alternate buffer should round-trip without garbage", async () => {
269
+ const { term, addon } = createTerminal(20, 5)
270
+
271
+ await writeAndWait(term, "normal\r\n")
272
+ await writeAndWait(term, "\x1b[?1049h\x1b[HALT")
273
+
274
+ expect(term.buffer.active.type).toBe("alternate")
275
+
276
+ const serialized = addon.serialize()
277
+
278
+ const { term: term2 } = createTerminal(20, 5)
279
+ terminals.push(term2)
280
+ await writeAndWait(term2, serialized)
281
+
282
+ expect(term2.buffer.active.type).toBe("alternate")
283
+
284
+ const line = term2.buffer.active.getLine(0)
285
+ expect(line?.translateToString(true)).toBe("ALT")
286
+
287
+ // Ensure a cell beyond content isn't garbage
288
+ const cellCode = line?.getCell(10)?.getCode()
289
+ expect(cellCode === 0 || cellCode === 32).toBe(true)
290
+ })
291
+
292
+ test("serialized output written to new terminal should match original colors", async () => {
293
+ const { term, addon } = createTerminal(40, 5)
294
+
295
+ const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! "
296
+ await writeAndWait(term, input)
297
+
298
+ const origLine = term.buffer.active.getLine(0)
299
+ const origHelloFg = origLine!.getCell(0)!.getFgColor()
300
+ const origWorldFg = origLine!.getCell(6)!.getFgColor()
301
+
302
+ const serialized = addon.serialize({ range: { start: 0, end: 0 } })
303
+
304
+ const { term: term2 } = createTerminal(40, 5)
305
+ terminals.push(term2)
306
+ await writeAndWait(term2, serialized)
307
+
308
+ const newLine = term2.buffer.active.getLine(0)
309
+
310
+ expect(newLine!.getCell(0)!.getChars()).toBe("H")
311
+ expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg)
312
+
313
+ expect(newLine!.getCell(6)!.getChars()).toBe("W")
314
+ expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg)
315
+
316
+ expect(newLine!.getCell(11)!.getChars()).toBe("!")
317
+ })
318
+ })
319
+ })