@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.
- package/AGENTS.md +30 -0
- package/README.md +51 -0
- package/bunfig.toml +2 -0
- package/e2e/context.spec.ts +45 -0
- package/e2e/file-open.spec.ts +23 -0
- package/e2e/file-viewer.spec.ts +35 -0
- package/e2e/fixtures.ts +40 -0
- package/e2e/home.spec.ts +21 -0
- package/e2e/model-picker.spec.ts +43 -0
- package/e2e/navigation.spec.ts +9 -0
- package/e2e/palette.spec.ts +15 -0
- package/e2e/prompt-mention.spec.ts +26 -0
- package/e2e/prompt-slash-open.spec.ts +22 -0
- package/e2e/prompt.spec.ts +62 -0
- package/e2e/session.spec.ts +21 -0
- package/e2e/settings.spec.ts +44 -0
- package/e2e/sidebar.spec.ts +21 -0
- package/e2e/terminal-init.spec.ts +25 -0
- package/e2e/terminal.spec.ts +16 -0
- package/e2e/tsconfig.json +8 -0
- package/e2e/utils.ts +38 -0
- package/happydom.ts +75 -0
- package/index.html +23 -0
- package/package.json +72 -0
- package/playwright.config.ts +43 -0
- package/public/_headers +17 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/oc-theme-preload.js +28 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/e2e-local.ts +143 -0
- package/src/addons/serialize.test.ts +319 -0
- package/src/addons/serialize.ts +591 -0
- package/src/app.tsx +150 -0
- package/src/components/dialog-connect-provider.tsx +428 -0
- package/src/components/dialog-edit-project.tsx +259 -0
- package/src/components/dialog-fork.tsx +104 -0
- package/src/components/dialog-manage-models.tsx +59 -0
- package/src/components/dialog-select-directory.tsx +208 -0
- package/src/components/dialog-select-file.tsx +196 -0
- package/src/components/dialog-select-mcp.tsx +96 -0
- package/src/components/dialog-select-model-unpaid.tsx +130 -0
- package/src/components/dialog-select-model.tsx +162 -0
- package/src/components/dialog-select-provider.tsx +70 -0
- package/src/components/dialog-select-server.tsx +249 -0
- package/src/components/dialog-settings.tsx +112 -0
- package/src/components/file-tree.tsx +112 -0
- package/src/components/link.tsx +17 -0
- package/src/components/model-tooltip.tsx +91 -0
- package/src/components/prompt-input.tsx +2076 -0
- package/src/components/session/index.ts +5 -0
- package/src/components/session/session-context-tab.tsx +428 -0
- package/src/components/session/session-header.tsx +343 -0
- package/src/components/session/session-new-view.tsx +93 -0
- package/src/components/session/session-sortable-tab.tsx +56 -0
- package/src/components/session/session-sortable-terminal-tab.tsx +187 -0
- package/src/components/session-context-usage.tsx +113 -0
- package/src/components/session-lsp-indicator.tsx +42 -0
- package/src/components/session-mcp-indicator.tsx +34 -0
- package/src/components/settings-agents.tsx +15 -0
- package/src/components/settings-commands.tsx +15 -0
- package/src/components/settings-general.tsx +306 -0
- package/src/components/settings-keybinds.tsx +437 -0
- package/src/components/settings-mcp.tsx +15 -0
- package/src/components/settings-models.tsx +15 -0
- package/src/components/settings-permissions.tsx +234 -0
- package/src/components/settings-providers.tsx +15 -0
- package/src/components/terminal.tsx +315 -0
- package/src/components/titlebar.tsx +156 -0
- package/src/context/command.tsx +308 -0
- package/src/context/comments.tsx +140 -0
- package/src/context/file.tsx +409 -0
- package/src/context/global-sdk.tsx +106 -0
- package/src/context/global-sync.tsx +898 -0
- package/src/context/language.tsx +161 -0
- package/src/context/layout-scroll.test.ts +73 -0
- package/src/context/layout-scroll.ts +118 -0
- package/src/context/layout.tsx +648 -0
- package/src/context/local.tsx +578 -0
- package/src/context/notification.tsx +173 -0
- package/src/context/permission.tsx +167 -0
- package/src/context/platform.tsx +59 -0
- package/src/context/prompt.tsx +245 -0
- package/src/context/sdk.tsx +48 -0
- package/src/context/server.tsx +214 -0
- package/src/context/settings.tsx +166 -0
- package/src/context/sync.tsx +320 -0
- package/src/context/terminal.tsx +267 -0
- package/src/custom-elements.d.ts +17 -0
- package/src/entry.tsx +76 -0
- package/src/env.d.ts +8 -0
- package/src/hooks/use-providers.ts +31 -0
- package/src/i18n/ar.ts +656 -0
- package/src/i18n/br.ts +667 -0
- package/src/i18n/da.ts +582 -0
- package/src/i18n/de.ts +591 -0
- package/src/i18n/en.ts +665 -0
- package/src/i18n/es.ts +585 -0
- package/src/i18n/fr.ts +592 -0
- package/src/i18n/ja.ts +579 -0
- package/src/i18n/ko.ts +580 -0
- package/src/i18n/no.ts +602 -0
- package/src/i18n/pl.ts +661 -0
- package/src/i18n/ru.ts +664 -0
- package/src/i18n/zh.ts +574 -0
- package/src/i18n/zht.ts +570 -0
- package/src/index.css +57 -0
- package/src/index.ts +2 -0
- package/src/pages/directory-layout.tsx +57 -0
- package/src/pages/error.tsx +290 -0
- package/src/pages/home.tsx +125 -0
- package/src/pages/layout.tsx +2599 -0
- package/src/pages/session.tsx +2505 -0
- package/src/sst-env.d.ts +10 -0
- package/src/utils/dom.ts +51 -0
- package/src/utils/id.ts +99 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/perf.ts +135 -0
- package/src/utils/persist.ts +377 -0
- package/src/utils/prompt.ts +203 -0
- package/src/utils/same.ts +6 -0
- package/src/utils/solid-dnd.tsx +55 -0
- package/src/utils/sound.ts +110 -0
- package/src/utils/speech.ts +302 -0
- package/src/utils/worktree.ts +58 -0
- package/sst-env.d.ts +9 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +15 -0
- package/vite.js +26 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
|
|
2
|
+
import { createStore, produce } from "solid-js/store"
|
|
3
|
+
import { createSimpleContext } from "@jonsoc/ui/context"
|
|
4
|
+
import type { FileContent } from "@jonsoc/sdk/v2"
|
|
5
|
+
import { showToast } from "@jonsoc/ui/toast"
|
|
6
|
+
import { useParams } from "@solidjs/router"
|
|
7
|
+
import { getFilename } from "@jonsoc/util/path"
|
|
8
|
+
import { useSDK } from "./sdk"
|
|
9
|
+
import { useSync } from "./sync"
|
|
10
|
+
import { useLanguage } from "@/context/language"
|
|
11
|
+
import { Persist, persisted } from "@/utils/persist"
|
|
12
|
+
|
|
13
|
+
export type FileSelection = {
|
|
14
|
+
startLine: number
|
|
15
|
+
startChar: number
|
|
16
|
+
endLine: number
|
|
17
|
+
endChar: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SelectedLineRange = {
|
|
21
|
+
start: number
|
|
22
|
+
end: number
|
|
23
|
+
side?: "additions" | "deletions"
|
|
24
|
+
endSide?: "additions" | "deletions"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FileViewState = {
|
|
28
|
+
scrollTop?: number
|
|
29
|
+
scrollLeft?: number
|
|
30
|
+
selectedLines?: SelectedLineRange | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type FileState = {
|
|
34
|
+
path: string
|
|
35
|
+
name: string
|
|
36
|
+
loaded?: boolean
|
|
37
|
+
loading?: boolean
|
|
38
|
+
error?: string
|
|
39
|
+
content?: FileContent
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripFileProtocol(input: string) {
|
|
43
|
+
if (!input.startsWith("file://")) return input
|
|
44
|
+
return input.slice("file://".length)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripQueryAndHash(input: string) {
|
|
48
|
+
const hashIndex = input.indexOf("#")
|
|
49
|
+
const queryIndex = input.indexOf("?")
|
|
50
|
+
|
|
51
|
+
if (hashIndex !== -1 && queryIndex !== -1) {
|
|
52
|
+
return input.slice(0, Math.min(hashIndex, queryIndex))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hashIndex !== -1) return input.slice(0, hashIndex)
|
|
56
|
+
if (queryIndex !== -1) return input.slice(0, queryIndex)
|
|
57
|
+
return input
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function selectionFromLines(range: SelectedLineRange): FileSelection {
|
|
61
|
+
const startLine = Math.min(range.start, range.end)
|
|
62
|
+
const endLine = Math.max(range.start, range.end)
|
|
63
|
+
return {
|
|
64
|
+
startLine,
|
|
65
|
+
endLine,
|
|
66
|
+
startChar: 0,
|
|
67
|
+
endChar: 0,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
|
|
72
|
+
if (range.start <= range.end) return range
|
|
73
|
+
|
|
74
|
+
const startSide = range.side
|
|
75
|
+
const endSide = range.endSide ?? startSide
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...range,
|
|
79
|
+
start: range.end,
|
|
80
|
+
end: range.start,
|
|
81
|
+
side: endSide,
|
|
82
|
+
endSide: startSide !== endSide ? startSide : undefined,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const WORKSPACE_KEY = "__workspace__"
|
|
87
|
+
const MAX_FILE_VIEW_SESSIONS = 20
|
|
88
|
+
const MAX_VIEW_FILES = 500
|
|
89
|
+
|
|
90
|
+
type ViewSession = ReturnType<typeof createViewSession>
|
|
91
|
+
|
|
92
|
+
type ViewCacheEntry = {
|
|
93
|
+
value: ViewSession
|
|
94
|
+
dispose: VoidFunction
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createViewSession(dir: string, id: string | undefined) {
|
|
98
|
+
const legacyViewKey = `${dir}/file${id ? "/" + id : ""}.v1`
|
|
99
|
+
|
|
100
|
+
const [view, setView, _, ready] = persisted(
|
|
101
|
+
Persist.scoped(dir, id, "file-view", [legacyViewKey]),
|
|
102
|
+
createStore<{
|
|
103
|
+
file: Record<string, FileViewState>
|
|
104
|
+
}>({
|
|
105
|
+
file: {},
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const meta = { pruned: false }
|
|
110
|
+
|
|
111
|
+
const pruneView = (keep?: string) => {
|
|
112
|
+
const keys = Object.keys(view.file)
|
|
113
|
+
if (keys.length <= MAX_VIEW_FILES) return
|
|
114
|
+
|
|
115
|
+
const drop = keys.filter((key) => key !== keep).slice(0, keys.length - MAX_VIEW_FILES)
|
|
116
|
+
if (drop.length === 0) return
|
|
117
|
+
|
|
118
|
+
setView(
|
|
119
|
+
produce((draft) => {
|
|
120
|
+
for (const key of drop) {
|
|
121
|
+
delete draft.file[key]
|
|
122
|
+
}
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
createEffect(() => {
|
|
128
|
+
if (!ready()) return
|
|
129
|
+
if (meta.pruned) return
|
|
130
|
+
meta.pruned = true
|
|
131
|
+
pruneView()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const scrollTop = (path: string) => view.file[path]?.scrollTop
|
|
135
|
+
const scrollLeft = (path: string) => view.file[path]?.scrollLeft
|
|
136
|
+
const selectedLines = (path: string) => view.file[path]?.selectedLines
|
|
137
|
+
|
|
138
|
+
const setScrollTop = (path: string, top: number) => {
|
|
139
|
+
setView("file", path, (current) => {
|
|
140
|
+
if (current?.scrollTop === top) return current
|
|
141
|
+
return {
|
|
142
|
+
...(current ?? {}),
|
|
143
|
+
scrollTop: top,
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
pruneView(path)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const setScrollLeft = (path: string, left: number) => {
|
|
150
|
+
setView("file", path, (current) => {
|
|
151
|
+
if (current?.scrollLeft === left) return current
|
|
152
|
+
return {
|
|
153
|
+
...(current ?? {}),
|
|
154
|
+
scrollLeft: left,
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
pruneView(path)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const setSelectedLines = (path: string, range: SelectedLineRange | null) => {
|
|
161
|
+
const next = range ? normalizeSelectedLines(range) : null
|
|
162
|
+
setView("file", path, (current) => {
|
|
163
|
+
if (current?.selectedLines === next) return current
|
|
164
|
+
return {
|
|
165
|
+
...(current ?? {}),
|
|
166
|
+
selectedLines: next,
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
pruneView(path)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
ready,
|
|
174
|
+
scrollTop,
|
|
175
|
+
scrollLeft,
|
|
176
|
+
selectedLines,
|
|
177
|
+
setScrollTop,
|
|
178
|
+
setScrollLeft,
|
|
179
|
+
setSelectedLines,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|
184
|
+
name: "File",
|
|
185
|
+
gate: false,
|
|
186
|
+
init: () => {
|
|
187
|
+
const sdk = useSDK()
|
|
188
|
+
const sync = useSync()
|
|
189
|
+
const params = useParams()
|
|
190
|
+
const language = useLanguage()
|
|
191
|
+
|
|
192
|
+
const scope = createMemo(() => sdk.directory)
|
|
193
|
+
|
|
194
|
+
const directory = createMemo(() => sync.data.path.directory)
|
|
195
|
+
|
|
196
|
+
function normalize(input: string) {
|
|
197
|
+
const root = directory()
|
|
198
|
+
const prefix = root.endsWith("/") ? root : root + "/"
|
|
199
|
+
|
|
200
|
+
let path = stripQueryAndHash(stripFileProtocol(input))
|
|
201
|
+
|
|
202
|
+
if (path.startsWith(prefix)) {
|
|
203
|
+
path = path.slice(prefix.length)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (path.startsWith(root)) {
|
|
207
|
+
path = path.slice(root.length)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (path.startsWith("./")) {
|
|
211
|
+
path = path.slice(2)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (path.startsWith("/")) {
|
|
215
|
+
path = path.slice(1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return path
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function tab(input: string) {
|
|
222
|
+
const path = normalize(input)
|
|
223
|
+
return `file://${path}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function pathFromTab(tabValue: string) {
|
|
227
|
+
if (!tabValue.startsWith("file://")) return
|
|
228
|
+
return normalize(tabValue)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const inflight = new Map<string, Promise<void>>()
|
|
232
|
+
|
|
233
|
+
const [store, setStore] = createStore<{
|
|
234
|
+
file: Record<string, FileState>
|
|
235
|
+
}>({
|
|
236
|
+
file: {},
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
createEffect(() => {
|
|
240
|
+
scope()
|
|
241
|
+
inflight.clear()
|
|
242
|
+
setStore("file", {})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const viewCache = new Map<string, ViewCacheEntry>()
|
|
246
|
+
|
|
247
|
+
const disposeViews = () => {
|
|
248
|
+
for (const entry of viewCache.values()) {
|
|
249
|
+
entry.dispose()
|
|
250
|
+
}
|
|
251
|
+
viewCache.clear()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const pruneViews = () => {
|
|
255
|
+
while (viewCache.size > MAX_FILE_VIEW_SESSIONS) {
|
|
256
|
+
const first = viewCache.keys().next().value
|
|
257
|
+
if (!first) return
|
|
258
|
+
const entry = viewCache.get(first)
|
|
259
|
+
entry?.dispose()
|
|
260
|
+
viewCache.delete(first)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const loadView = (dir: string, id: string | undefined) => {
|
|
265
|
+
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
|
266
|
+
const existing = viewCache.get(key)
|
|
267
|
+
if (existing) {
|
|
268
|
+
viewCache.delete(key)
|
|
269
|
+
viewCache.set(key, existing)
|
|
270
|
+
return existing.value
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entry = createRoot((dispose) => ({
|
|
274
|
+
value: createViewSession(dir, id),
|
|
275
|
+
dispose,
|
|
276
|
+
}))
|
|
277
|
+
|
|
278
|
+
viewCache.set(key, entry)
|
|
279
|
+
pruneViews()
|
|
280
|
+
return entry.value
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const view = createMemo(() => loadView(params.dir!, params.id))
|
|
284
|
+
|
|
285
|
+
function ensure(path: string) {
|
|
286
|
+
if (!path) return
|
|
287
|
+
if (store.file[path]) return
|
|
288
|
+
setStore("file", path, { path, name: getFilename(path) })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function load(input: string, options?: { force?: boolean }) {
|
|
292
|
+
const path = normalize(input)
|
|
293
|
+
if (!path) return Promise.resolve()
|
|
294
|
+
|
|
295
|
+
const directory = scope()
|
|
296
|
+
const key = `${directory}\n${path}`
|
|
297
|
+
const client = sdk.client
|
|
298
|
+
|
|
299
|
+
ensure(path)
|
|
300
|
+
|
|
301
|
+
const current = store.file[path]
|
|
302
|
+
if (!options?.force && current?.loaded) return Promise.resolve()
|
|
303
|
+
|
|
304
|
+
const pending = inflight.get(key)
|
|
305
|
+
if (pending) return pending
|
|
306
|
+
|
|
307
|
+
setStore(
|
|
308
|
+
"file",
|
|
309
|
+
path,
|
|
310
|
+
produce((draft) => {
|
|
311
|
+
draft.loading = true
|
|
312
|
+
draft.error = undefined
|
|
313
|
+
}),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const promise = client.file
|
|
317
|
+
.read({ path })
|
|
318
|
+
.then((x) => {
|
|
319
|
+
if (scope() !== directory) return
|
|
320
|
+
setStore(
|
|
321
|
+
"file",
|
|
322
|
+
path,
|
|
323
|
+
produce((draft) => {
|
|
324
|
+
draft.loaded = true
|
|
325
|
+
draft.loading = false
|
|
326
|
+
draft.content = x.data
|
|
327
|
+
}),
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
.catch((e) => {
|
|
331
|
+
if (scope() !== directory) return
|
|
332
|
+
setStore(
|
|
333
|
+
"file",
|
|
334
|
+
path,
|
|
335
|
+
produce((draft) => {
|
|
336
|
+
draft.loading = false
|
|
337
|
+
draft.error = e.message
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
showToast({
|
|
341
|
+
variant: "error",
|
|
342
|
+
title: language.t("toast.file.loadFailed.title"),
|
|
343
|
+
description: e.message,
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
.finally(() => {
|
|
347
|
+
inflight.delete(key)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
inflight.set(key, promise)
|
|
351
|
+
return promise
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const stop = sdk.event.listen((e) => {
|
|
355
|
+
const event = e.details
|
|
356
|
+
if (event.type !== "file.watcher.updated") return
|
|
357
|
+
const path = normalize(event.properties.file)
|
|
358
|
+
if (!path) return
|
|
359
|
+
if (path.startsWith(".git/")) return
|
|
360
|
+
if (!store.file[path]) return
|
|
361
|
+
load(path, { force: true })
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const get = (input: string) => store.file[normalize(input)]
|
|
365
|
+
|
|
366
|
+
const scrollTop = (input: string) => view().scrollTop(normalize(input))
|
|
367
|
+
const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
|
|
368
|
+
const selectedLines = (input: string) => view().selectedLines(normalize(input))
|
|
369
|
+
|
|
370
|
+
const setScrollTop = (input: string, top: number) => {
|
|
371
|
+
const path = normalize(input)
|
|
372
|
+
view().setScrollTop(path, top)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const setScrollLeft = (input: string, left: number) => {
|
|
376
|
+
const path = normalize(input)
|
|
377
|
+
view().setScrollLeft(path, left)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
|
|
381
|
+
const path = normalize(input)
|
|
382
|
+
view().setSelectedLines(path, range)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
onCleanup(() => {
|
|
386
|
+
stop()
|
|
387
|
+
disposeViews()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
ready: () => view().ready(),
|
|
392
|
+
normalize,
|
|
393
|
+
tab,
|
|
394
|
+
pathFromTab,
|
|
395
|
+
get,
|
|
396
|
+
load,
|
|
397
|
+
scrollTop,
|
|
398
|
+
scrollLeft,
|
|
399
|
+
setScrollTop,
|
|
400
|
+
setScrollLeft,
|
|
401
|
+
selectedLines,
|
|
402
|
+
setSelectedLines,
|
|
403
|
+
searchFiles: (query: string) =>
|
|
404
|
+
sdk.client.find.files({ query, dirs: "false" }).then((x) => (x.data ?? []).map(normalize)),
|
|
405
|
+
searchFilesAndDirectories: (query: string) =>
|
|
406
|
+
sdk.client.find.files({ query, dirs: "true" }).then((x) => (x.data ?? []).map(normalize)),
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createOpencodeClient, type Event } from "@jonsoc/sdk/v2/client"
|
|
2
|
+
import { createSimpleContext } from "@jonsoc/ui/context"
|
|
3
|
+
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
|
4
|
+
import { batch, onCleanup } from "solid-js"
|
|
5
|
+
import { usePlatform } from "./platform"
|
|
6
|
+
import { useServer } from "./server"
|
|
7
|
+
|
|
8
|
+
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
|
9
|
+
name: "GlobalSDK",
|
|
10
|
+
init: () => {
|
|
11
|
+
const server = useServer()
|
|
12
|
+
const platform = usePlatform()
|
|
13
|
+
const abort = new AbortController()
|
|
14
|
+
|
|
15
|
+
const eventSdk = createOpencodeClient({
|
|
16
|
+
baseUrl: server.url,
|
|
17
|
+
signal: abort.signal,
|
|
18
|
+
fetch: platform.fetch,
|
|
19
|
+
})
|
|
20
|
+
const emitter = createGlobalEmitter<{
|
|
21
|
+
[key: string]: Event
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
type Queued = { directory: string; payload: Event }
|
|
25
|
+
|
|
26
|
+
let queue: Array<Queued | undefined> = []
|
|
27
|
+
const coalesced = new Map<string, number>()
|
|
28
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
29
|
+
let last = 0
|
|
30
|
+
|
|
31
|
+
const key = (directory: string, payload: Event) => {
|
|
32
|
+
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
|
|
33
|
+
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
|
|
34
|
+
if (payload.type === "message.part.updated") {
|
|
35
|
+
const part = payload.properties.part
|
|
36
|
+
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const flush = () => {
|
|
41
|
+
if (timer) clearTimeout(timer)
|
|
42
|
+
timer = undefined
|
|
43
|
+
|
|
44
|
+
const events = queue
|
|
45
|
+
queue = []
|
|
46
|
+
coalesced.clear()
|
|
47
|
+
if (events.length === 0) return
|
|
48
|
+
|
|
49
|
+
last = Date.now()
|
|
50
|
+
batch(() => {
|
|
51
|
+
for (const event of events) {
|
|
52
|
+
if (!event) continue
|
|
53
|
+
emitter.emit(event.directory, event.payload)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const schedule = () => {
|
|
59
|
+
if (timer) return
|
|
60
|
+
const elapsed = Date.now() - last
|
|
61
|
+
timer = setTimeout(flush, Math.max(0, 16 - elapsed))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stop = () => {
|
|
65
|
+
flush()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void (async () => {
|
|
69
|
+
const events = await eventSdk.global.event()
|
|
70
|
+
let yielded = Date.now()
|
|
71
|
+
for await (const event of events.stream) {
|
|
72
|
+
const directory = event.directory ?? "global"
|
|
73
|
+
const payload = event.payload
|
|
74
|
+
const k = key(directory, payload)
|
|
75
|
+
if (k) {
|
|
76
|
+
const i = coalesced.get(k)
|
|
77
|
+
if (i !== undefined) {
|
|
78
|
+
queue[i] = undefined
|
|
79
|
+
}
|
|
80
|
+
coalesced.set(k, queue.length)
|
|
81
|
+
}
|
|
82
|
+
queue.push({ directory, payload })
|
|
83
|
+
schedule()
|
|
84
|
+
|
|
85
|
+
if (Date.now() - yielded < 8) continue
|
|
86
|
+
yielded = Date.now()
|
|
87
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
|
88
|
+
}
|
|
89
|
+
})()
|
|
90
|
+
.finally(stop)
|
|
91
|
+
.catch(() => undefined)
|
|
92
|
+
|
|
93
|
+
onCleanup(() => {
|
|
94
|
+
abort.abort()
|
|
95
|
+
stop()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const sdk = createOpencodeClient({
|
|
99
|
+
baseUrl: server.url,
|
|
100
|
+
fetch: platform.fetch,
|
|
101
|
+
throwOnError: true,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return { url: server.url, client: sdk, event: emitter }
|
|
105
|
+
},
|
|
106
|
+
})
|