@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,267 @@
1
+ import { createStore, produce } from "solid-js/store"
2
+ import { createSimpleContext } from "@jonsoc/ui/context"
3
+ import { batch, createEffect, createMemo, createRoot, onCleanup } from "solid-js"
4
+ import { useParams } from "@solidjs/router"
5
+ import { useSDK } from "./sdk"
6
+ import { Persist, persisted } from "@/utils/persist"
7
+
8
+ export type LocalPTY = {
9
+ id: string
10
+ title: string
11
+ titleNumber: number
12
+ rows?: number
13
+ cols?: number
14
+ buffer?: string
15
+ scrollY?: number
16
+ error?: boolean
17
+ }
18
+
19
+ const WORKSPACE_KEY = "__workspace__"
20
+ const MAX_TERMINAL_SESSIONS = 20
21
+
22
+ type TerminalSession = ReturnType<typeof createTerminalSession>
23
+
24
+ type TerminalCacheEntry = {
25
+ value: TerminalSession
26
+ dispose: VoidFunction
27
+ }
28
+
29
+ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, session?: string) {
30
+ const legacy = session ? [`${dir}/terminal/${session}.v1`, `${dir}/terminal.v1`] : [`${dir}/terminal.v1`]
31
+
32
+ const numberFromTitle = (title: string) => {
33
+ const match = title.match(/^Terminal (\d+)$/)
34
+ if (!match) return
35
+ const value = Number(match[1])
36
+ if (!Number.isFinite(value) || value <= 0) return
37
+ return value
38
+ }
39
+
40
+ const [store, setStore, _, ready] = persisted(
41
+ Persist.workspace(dir, "terminal", legacy),
42
+ createStore<{
43
+ active?: string
44
+ all: LocalPTY[]
45
+ }>({
46
+ all: [],
47
+ }),
48
+ )
49
+
50
+ const unsub = sdk.event.on("pty.exited", (event) => {
51
+ const id = event.properties.id
52
+ if (!store.all.some((x) => x.id === id)) return
53
+ batch(() => {
54
+ setStore(
55
+ "all",
56
+ store.all.filter((x) => x.id !== id),
57
+ )
58
+ if (store.active === id) {
59
+ const remaining = store.all.filter((x) => x.id !== id)
60
+ setStore("active", remaining[0]?.id)
61
+ }
62
+ })
63
+ })
64
+ onCleanup(unsub)
65
+
66
+ const meta = { migrated: false }
67
+
68
+ createEffect(() => {
69
+ if (!ready()) return
70
+ if (meta.migrated) return
71
+ meta.migrated = true
72
+
73
+ setStore("all", (all) => {
74
+ const next = all.map((pty) => {
75
+ const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
76
+ if (direct !== undefined) return pty
77
+ const parsed = numberFromTitle(pty.title)
78
+ if (parsed === undefined) return pty
79
+ return { ...pty, titleNumber: parsed }
80
+ })
81
+ if (next.every((pty, index) => pty === all[index])) return all
82
+ return next
83
+ })
84
+ })
85
+
86
+ return {
87
+ ready,
88
+ all: createMemo(() => Object.values(store.all)),
89
+ active: createMemo(() => store.active),
90
+ new() {
91
+ const existingTitleNumbers = new Set(
92
+ store.all.flatMap((pty) => {
93
+ const direct = Number.isFinite(pty.titleNumber) && pty.titleNumber > 0 ? pty.titleNumber : undefined
94
+ if (direct !== undefined) return [direct]
95
+ const parsed = numberFromTitle(pty.title)
96
+ if (parsed === undefined) return []
97
+ return [parsed]
98
+ }),
99
+ )
100
+
101
+ const nextNumber =
102
+ Array.from({ length: existingTitleNumbers.size + 1 }, (_, index) => index + 1).find(
103
+ (number) => !existingTitleNumbers.has(number),
104
+ ) ?? 1
105
+
106
+ sdk.client.pty
107
+ .create({ title: `Terminal ${nextNumber}` })
108
+ .then((pty) => {
109
+ const id = pty.data?.id
110
+ if (!id) return
111
+ const newTerminal = {
112
+ id,
113
+ title: pty.data?.title ?? "Terminal",
114
+ titleNumber: nextNumber,
115
+ }
116
+ setStore("all", (all) => {
117
+ const newAll = [...all, newTerminal]
118
+ return newAll
119
+ })
120
+ setStore("active", id)
121
+ })
122
+ .catch((e) => {
123
+ console.error("Failed to create terminal", e)
124
+ })
125
+ },
126
+ update(pty: Partial<LocalPTY> & { id: string }) {
127
+ const index = store.all.findIndex((x) => x.id === pty.id)
128
+ if (index !== -1) {
129
+ setStore("all", index, (existing) => ({ ...existing, ...pty }))
130
+ }
131
+ sdk.client.pty
132
+ .update({
133
+ ptyID: pty.id,
134
+ title: pty.title,
135
+ size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
136
+ })
137
+ .catch((e) => {
138
+ console.error("Failed to update terminal", e)
139
+ })
140
+ },
141
+ async clone(id: string) {
142
+ const index = store.all.findIndex((x) => x.id === id)
143
+ const pty = store.all[index]
144
+ if (!pty) return
145
+ const clone = await sdk.client.pty
146
+ .create({
147
+ title: pty.title,
148
+ })
149
+ .catch((e) => {
150
+ console.error("Failed to clone terminal", e)
151
+ return undefined
152
+ })
153
+ if (!clone?.data) return
154
+ setStore("all", index, {
155
+ ...pty,
156
+ ...clone.data,
157
+ })
158
+ if (store.active === pty.id) {
159
+ setStore("active", clone.data.id)
160
+ }
161
+ },
162
+ open(id: string) {
163
+ setStore("active", id)
164
+ },
165
+ next() {
166
+ const index = store.all.findIndex((x) => x.id === store.active)
167
+ if (index === -1) return
168
+ const nextIndex = (index + 1) % store.all.length
169
+ setStore("active", store.all[nextIndex]?.id)
170
+ },
171
+ previous() {
172
+ const index = store.all.findIndex((x) => x.id === store.active)
173
+ if (index === -1) return
174
+ const prevIndex = index === 0 ? store.all.length - 1 : index - 1
175
+ setStore("active", store.all[prevIndex]?.id)
176
+ },
177
+ async close(id: string) {
178
+ batch(() => {
179
+ const filtered = store.all.filter((x) => x.id !== id)
180
+ if (store.active === id) {
181
+ const index = store.all.findIndex((f) => f.id === id)
182
+ const next = index > 0 ? index - 1 : 0
183
+ setStore("active", filtered[next]?.id)
184
+ }
185
+ setStore("all", filtered)
186
+ })
187
+
188
+ await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
189
+ console.error("Failed to close terminal", e)
190
+ })
191
+ },
192
+ move(id: string, to: number) {
193
+ const index = store.all.findIndex((f) => f.id === id)
194
+ if (index === -1) return
195
+ setStore(
196
+ "all",
197
+ produce((all) => {
198
+ all.splice(to, 0, all.splice(index, 1)[0])
199
+ }),
200
+ )
201
+ },
202
+ }
203
+ }
204
+
205
+ export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
206
+ name: "Terminal",
207
+ gate: false,
208
+ init: () => {
209
+ const sdk = useSDK()
210
+ const params = useParams()
211
+ const cache = new Map<string, TerminalCacheEntry>()
212
+
213
+ const disposeAll = () => {
214
+ for (const entry of cache.values()) {
215
+ entry.dispose()
216
+ }
217
+ cache.clear()
218
+ }
219
+
220
+ onCleanup(disposeAll)
221
+
222
+ const prune = () => {
223
+ while (cache.size > MAX_TERMINAL_SESSIONS) {
224
+ const first = cache.keys().next().value
225
+ if (!first) return
226
+ const entry = cache.get(first)
227
+ entry?.dispose()
228
+ cache.delete(first)
229
+ }
230
+ }
231
+
232
+ const load = (dir: string, session?: string) => {
233
+ const key = `${dir}:${WORKSPACE_KEY}`
234
+ const existing = cache.get(key)
235
+ if (existing) {
236
+ cache.delete(key)
237
+ cache.set(key, existing)
238
+ return existing.value
239
+ }
240
+
241
+ const entry = createRoot((dispose) => ({
242
+ value: createTerminalSession(sdk, dir, session),
243
+ dispose,
244
+ }))
245
+
246
+ cache.set(key, entry)
247
+ prune()
248
+ return entry.value
249
+ }
250
+
251
+ const workspace = createMemo(() => load(params.dir!, params.id))
252
+
253
+ return {
254
+ ready: () => workspace().ready(),
255
+ all: () => workspace().all(),
256
+ active: () => workspace().active(),
257
+ new: () => workspace().new(),
258
+ update: (pty: Partial<LocalPTY> & { id: string }) => workspace().update(pty),
259
+ clone: (id: string) => workspace().clone(id),
260
+ open: (id: string) => workspace().open(id),
261
+ close: (id: string) => workspace().close(id),
262
+ move: (id: string, to: number) => workspace().move(id, to),
263
+ next: () => workspace().next(),
264
+ previous: () => workspace().previous(),
265
+ }
266
+ },
267
+ })
@@ -0,0 +1,17 @@
1
+ import { DIFFS_TAG_NAME } from "@pierre/diffs"
2
+
3
+ /**
4
+ * TypeScript declaration for the <diffs-container> custom element.
5
+ * This tells TypeScript that <diffs-container> is a valid JSX element in SolidJS.
6
+ * Required for using the @pierre/diffs web component in .tsx files.
7
+ */
8
+
9
+ declare module "solid-js" {
10
+ namespace JSX {
11
+ interface IntrinsicElements {
12
+ [DIFFS_TAG_NAME]: HTMLAttributes<HTMLElement>
13
+ }
14
+ }
15
+ }
16
+
17
+ export {}
package/src/entry.tsx ADDED
@@ -0,0 +1,76 @@
1
+ // @refresh reload
2
+ import { render } from "solid-js/web"
3
+ import { AppBaseProviders, AppInterface } from "@/app"
4
+ import { Platform, PlatformProvider } from "@/context/platform"
5
+ import { dict as en } from "@/i18n/en"
6
+ import { dict as zh } from "@/i18n/zh"
7
+ import pkg from "../package.json"
8
+
9
+ const root = document.getElementById("root")
10
+ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
11
+ const locale = (() => {
12
+ if (typeof navigator !== "object") return "en" as const
13
+ const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
14
+ for (const language of languages) {
15
+ if (!language) continue
16
+ if (language.toLowerCase().startsWith("zh")) return "zh" as const
17
+ }
18
+ return "en" as const
19
+ })()
20
+
21
+ const key = "error.dev.rootNotFound" as const
22
+ const message = locale === "zh" ? (zh[key] ?? en[key]) : en[key]
23
+ throw new Error(message)
24
+ }
25
+
26
+ const platform: Platform = {
27
+ platform: "web",
28
+ version: pkg.version,
29
+ openLink(url: string) {
30
+ window.open(url, "_blank")
31
+ },
32
+ restart: async () => {
33
+ window.location.reload()
34
+ },
35
+ notify: async (title, description, href) => {
36
+ if (!("Notification" in window)) return
37
+
38
+ const permission =
39
+ Notification.permission === "default"
40
+ ? await Notification.requestPermission().catch(() => "denied")
41
+ : Notification.permission
42
+
43
+ if (permission !== "granted") return
44
+
45
+ const inView = document.visibilityState === "visible" && document.hasFocus()
46
+ if (inView) return
47
+
48
+ await Promise.resolve()
49
+ .then(() => {
50
+ const notification = new Notification(title, {
51
+ body: description ?? "",
52
+ icon: "https://jonsoc.com/favicon-96x96-v3.png",
53
+ })
54
+ notification.onclick = () => {
55
+ window.focus()
56
+ if (href) {
57
+ window.history.pushState(null, "", href)
58
+ window.dispatchEvent(new PopStateEvent("popstate"))
59
+ }
60
+ notification.close()
61
+ }
62
+ })
63
+ .catch(() => undefined)
64
+ },
65
+ }
66
+
67
+ render(
68
+ () => (
69
+ <PlatformProvider value={platform}>
70
+ <AppBaseProviders>
71
+ <AppInterface />
72
+ </AppBaseProviders>
73
+ </PlatformProvider>
74
+ ),
75
+ root!,
76
+ )
package/src/env.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ interface ImportMetaEnv {
2
+ readonly VITE_OPENCODE_SERVER_HOST: string
3
+ readonly VITE_OPENCODE_SERVER_PORT: string
4
+ }
5
+
6
+ interface ImportMeta {
7
+ readonly env: ImportMetaEnv
8
+ }
@@ -0,0 +1,31 @@
1
+ import { useGlobalSync } from "@/context/global-sync"
2
+ import { base64Decode } from "@jonsoc/util/encode"
3
+ import { useParams } from "@solidjs/router"
4
+ import { createMemo } from "solid-js"
5
+
6
+ export const popularProviders = ["jonsoc", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
7
+
8
+ export function useProviders() {
9
+ const globalSync = useGlobalSync()
10
+ const params = useParams()
11
+ const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
12
+ const providers = createMemo(() => {
13
+ if (currentDirectory()) {
14
+ const [projectStore] = globalSync.child(currentDirectory())
15
+ return projectStore.provider
16
+ }
17
+ return globalSync.data.provider
18
+ })
19
+ const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
20
+ const paid = createMemo(() =>
21
+ connected().filter((p) => p.id !== "jonsoc" || Object.values(p.models).find((m) => m.cost?.input)),
22
+ )
23
+ const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
24
+ return {
25
+ all: createMemo(() => providers().all),
26
+ default: createMemo(() => providers().default),
27
+ popular,
28
+ connected,
29
+ paid,
30
+ }
31
+ }