@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,578 @@
1
+ import { createStore, produce, reconcile } from "solid-js/store"
2
+ import { batch, createEffect, createMemo, onCleanup } from "solid-js"
3
+ import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
4
+ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@jonsoc/sdk/v2"
5
+ import { createSimpleContext } from "@jonsoc/ui/context"
6
+ import { useSDK } from "./sdk"
7
+ import { useSync } from "./sync"
8
+ import { base64Encode } from "@jonsoc/util/encode"
9
+ import { useProviders } from "@/hooks/use-providers"
10
+ import { DateTime } from "luxon"
11
+ import { Persist, persisted } from "@/utils/persist"
12
+ import { showToast } from "@jonsoc/ui/toast"
13
+ import { useLanguage } from "@/context/language"
14
+
15
+ export type LocalFile = FileNode &
16
+ Partial<{
17
+ loaded: boolean
18
+ pinned: boolean
19
+ expanded: boolean
20
+ content: FileContent
21
+ selection: { startLine: number; startChar: number; endLine: number; endChar: number }
22
+ scrollTop: number
23
+ view: "raw" | "diff-unified" | "diff-split"
24
+ folded: string[]
25
+ selectedChange: number
26
+ status: FileStatus
27
+ }>
28
+ export type TextSelection = LocalFile["selection"]
29
+ export type View = LocalFile["view"]
30
+
31
+ export type LocalModel = Omit<Model, "provider"> & {
32
+ provider: Provider
33
+ latest?: boolean
34
+ }
35
+ export type ModelKey = { providerID: string; modelID: string }
36
+
37
+ export type FileContext = { type: "file"; path: string; selection?: TextSelection }
38
+ export type ContextItem = FileContext
39
+
40
+ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
41
+ name: "Local",
42
+ init: () => {
43
+ const sdk = useSDK()
44
+ const sync = useSync()
45
+ const providers = useProviders()
46
+ const language = useLanguage()
47
+
48
+ function isModelValid(model: ModelKey) {
49
+ const provider = providers.all().find((x) => x.id === model.providerID)
50
+ return (
51
+ !!provider?.models[model.modelID] &&
52
+ providers
53
+ .connected()
54
+ .map((p) => p.id)
55
+ .includes(model.providerID)
56
+ )
57
+ }
58
+
59
+ function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
60
+ for (const modelFn of modelFns) {
61
+ const model = modelFn()
62
+ if (!model) continue
63
+ if (isModelValid(model)) return model
64
+ }
65
+ }
66
+
67
+ const agent = (() => {
68
+ const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
69
+ const [store, setStore] = createStore<{
70
+ current?: string
71
+ }>({
72
+ current: list()[0]?.name,
73
+ })
74
+ return {
75
+ list,
76
+ current() {
77
+ const available = list()
78
+ if (available.length === 0) return undefined
79
+ return available.find((x) => x.name === store.current) ?? available[0]
80
+ },
81
+ set(name: string | undefined) {
82
+ const available = list()
83
+ if (available.length === 0) {
84
+ setStore("current", undefined)
85
+ return
86
+ }
87
+ if (name && available.some((x) => x.name === name)) {
88
+ setStore("current", name)
89
+ return
90
+ }
91
+ setStore("current", available[0].name)
92
+ },
93
+ move(direction: 1 | -1) {
94
+ const available = list()
95
+ if (available.length === 0) {
96
+ setStore("current", undefined)
97
+ return
98
+ }
99
+ let next = available.findIndex((x) => x.name === store.current) + direction
100
+ if (next < 0) next = available.length - 1
101
+ if (next >= available.length) next = 0
102
+ const value = available[next]
103
+ if (!value) return
104
+ setStore("current", value.name)
105
+ if (value.model)
106
+ model.set({
107
+ providerID: value.model.providerID,
108
+ modelID: value.model.modelID,
109
+ })
110
+ },
111
+ }
112
+ })()
113
+
114
+ const model = (() => {
115
+ const [store, setStore, _, modelReady] = persisted(
116
+ Persist.global("model", ["model.v1"]),
117
+ createStore<{
118
+ user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
119
+ recent: ModelKey[]
120
+ variant?: Record<string, string | undefined>
121
+ }>({
122
+ user: [],
123
+ recent: [],
124
+ variant: {},
125
+ }),
126
+ )
127
+
128
+ const [ephemeral, setEphemeral] = createStore<{
129
+ model: Record<string, ModelKey>
130
+ }>({
131
+ model: {},
132
+ })
133
+
134
+ const available = createMemo(() =>
135
+ providers.connected().flatMap((p) =>
136
+ Object.values(p.models).map((m) => ({
137
+ ...m,
138
+ provider: p,
139
+ })),
140
+ ),
141
+ )
142
+
143
+ const latest = createMemo(() =>
144
+ pipe(
145
+ available(),
146
+ filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
147
+ groupBy((x) => x.provider.id),
148
+ mapValues((models) =>
149
+ pipe(
150
+ models,
151
+ groupBy((x) => x.family),
152
+ values(),
153
+ (groups) =>
154
+ groups.flatMap((g) => {
155
+ const first = firstBy(g, [(x) => x.release_date, "desc"])
156
+ return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
157
+ }),
158
+ ),
159
+ ),
160
+ values(),
161
+ flat(),
162
+ ),
163
+ )
164
+
165
+ const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
166
+
167
+ const userVisibilityMap = createMemo(() => {
168
+ const map = new Map<string, "show" | "hide">()
169
+ for (const item of store.user) {
170
+ map.set(`${item.providerID}:${item.modelID}`, item.visibility)
171
+ }
172
+ return map
173
+ })
174
+
175
+ const list = createMemo(() =>
176
+ available().map((m) => ({
177
+ ...m,
178
+ name: m.name.replace("(latest)", "").trim(),
179
+ latest: m.name.includes("(latest)"),
180
+ })),
181
+ )
182
+
183
+ const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
184
+
185
+ const fallbackModel = createMemo(() => {
186
+ if (sync.data.config.model) {
187
+ const [providerID, modelID] = sync.data.config.model.split("/")
188
+ if (isModelValid({ providerID, modelID })) {
189
+ return {
190
+ providerID,
191
+ modelID,
192
+ }
193
+ }
194
+ }
195
+
196
+ for (const item of store.recent) {
197
+ if (isModelValid(item)) {
198
+ return item
199
+ }
200
+ }
201
+
202
+ for (const p of providers.connected()) {
203
+ if (p.id in providers.default()) {
204
+ return {
205
+ providerID: p.id,
206
+ modelID: providers.default()[p.id],
207
+ }
208
+ }
209
+ }
210
+
211
+ throw new Error("No default model found")
212
+ })
213
+
214
+ const current = createMemo(() => {
215
+ const a = agent.current()
216
+ if (!a) return undefined
217
+ const key = getFirstValidModel(
218
+ () => ephemeral.model[a.name],
219
+ () => a.model,
220
+ fallbackModel,
221
+ )
222
+ if (!key) return undefined
223
+ return find(key)
224
+ })
225
+
226
+ const recent = createMemo(() => store.recent.map(find).filter(Boolean))
227
+
228
+ const cycle = (direction: 1 | -1) => {
229
+ const recentList = recent()
230
+ const currentModel = current()
231
+ if (!currentModel) return
232
+
233
+ const index = recentList.findIndex(
234
+ (x) => x?.provider.id === currentModel.provider.id && x?.id === currentModel.id,
235
+ )
236
+ if (index === -1) return
237
+
238
+ let next = index + direction
239
+ if (next < 0) next = recentList.length - 1
240
+ if (next >= recentList.length) next = 0
241
+
242
+ const val = recentList[next]
243
+ if (!val) return
244
+
245
+ model.set({
246
+ providerID: val.provider.id,
247
+ modelID: val.id,
248
+ })
249
+ }
250
+
251
+ function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
252
+ const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
253
+ if (index >= 0) {
254
+ setStore("user", index, { visibility })
255
+ } else {
256
+ setStore("user", store.user.length, { ...model, visibility })
257
+ }
258
+ }
259
+
260
+ return {
261
+ ready: modelReady,
262
+ current,
263
+ recent,
264
+ list,
265
+ cycle,
266
+ set(model: ModelKey | undefined, options?: { recent?: boolean }) {
267
+ batch(() => {
268
+ const currentAgent = agent.current()
269
+ if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel())
270
+ if (model) updateVisibility(model, "show")
271
+ if (options?.recent && model) {
272
+ const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
273
+ if (uniq.length > 5) uniq.pop()
274
+ setStore("recent", uniq)
275
+ }
276
+ })
277
+ },
278
+ visible(model: ModelKey) {
279
+ const key = `${model.providerID}:${model.modelID}`
280
+ const visibility = userVisibilityMap().get(key)
281
+ if (visibility === "hide") return false
282
+ if (visibility === "show") return true
283
+ if (latestSet().has(key)) return true
284
+ // For models without valid release_date (e.g. custom models), show by default
285
+ const m = find(model)
286
+ if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
287
+ return false
288
+ },
289
+ setVisibility(model: ModelKey, visible: boolean) {
290
+ updateVisibility(model, visible ? "show" : "hide")
291
+ },
292
+ variant: {
293
+ current() {
294
+ const m = current()
295
+ if (!m) return undefined
296
+ const key = `${m.provider.id}/${m.id}`
297
+ return store.variant?.[key]
298
+ },
299
+ list() {
300
+ const m = current()
301
+ if (!m) return []
302
+ if (!m.variants) return []
303
+ return Object.keys(m.variants)
304
+ },
305
+ set(value: string | undefined) {
306
+ const m = current()
307
+ if (!m) return
308
+ const key = `${m.provider.id}/${m.id}`
309
+ if (!store.variant) {
310
+ setStore("variant", { [key]: value })
311
+ } else {
312
+ setStore("variant", key, value)
313
+ }
314
+ },
315
+ cycle() {
316
+ const variants = this.list()
317
+ if (variants.length === 0) return
318
+ const currentVariant = this.current()
319
+ if (!currentVariant) {
320
+ this.set(variants[0])
321
+ return
322
+ }
323
+ const index = variants.indexOf(currentVariant)
324
+ if (index === -1 || index === variants.length - 1) {
325
+ this.set(undefined)
326
+ return
327
+ }
328
+ this.set(variants[index + 1])
329
+ },
330
+ },
331
+ }
332
+ })()
333
+
334
+ const file = (() => {
335
+ const [store, setStore] = createStore<{
336
+ node: Record<string, LocalFile>
337
+ }>({
338
+ node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
339
+ })
340
+
341
+ const scope = createMemo(() => sdk.directory)
342
+ createEffect(() => {
343
+ scope()
344
+ setStore("node", {})
345
+ })
346
+
347
+ // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
348
+ // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
349
+
350
+ // createEffect((prev: FileStatus[]) => {
351
+ // const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
352
+ // for (const p of removed) {
353
+ // setStore(
354
+ // "node",
355
+ // p.path,
356
+ // produce((draft) => {
357
+ // draft.status = undefined
358
+ // draft.view = "raw"
359
+ // }),
360
+ // )
361
+ // load(p.path)
362
+ // }
363
+ // for (const p of sync.data.changes) {
364
+ // if (store.node[p.path] === undefined) {
365
+ // fetch(p.path).then(() => {
366
+ // if (store.node[p.path] === undefined) return
367
+ // setStore("node", p.path, "status", p)
368
+ // })
369
+ // } else {
370
+ // setStore("node", p.path, "status", p)
371
+ // }
372
+ // }
373
+ // return sync.data.changes
374
+ // }, sync.data.changes)
375
+
376
+ // const changed = (path: string) => {
377
+ // const node = store.node[path]
378
+ // if (node?.status) return true
379
+ // const set = changeset()
380
+ // if (set.has(path)) return true
381
+ // for (const p of set) {
382
+ // if (p.startsWith(path ? path + "/" : "")) return true
383
+ // }
384
+ // return false
385
+ // }
386
+
387
+ // const resetNode = (path: string) => {
388
+ // setStore("node", path, {
389
+ // loaded: undefined,
390
+ // pinned: undefined,
391
+ // content: undefined,
392
+ // selection: undefined,
393
+ // scrollTop: undefined,
394
+ // folded: undefined,
395
+ // view: undefined,
396
+ // selectedChange: undefined,
397
+ // })
398
+ // }
399
+
400
+ const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
401
+
402
+ const load = async (path: string) => {
403
+ const directory = scope()
404
+ const client = sdk.client
405
+ const relativePath = relative(path)
406
+ await client.file
407
+ .read({ path: relativePath })
408
+ .then((x) => {
409
+ if (scope() !== directory) return
410
+ if (!store.node[relativePath]) return
411
+ setStore(
412
+ "node",
413
+ relativePath,
414
+ produce((draft) => {
415
+ draft.loaded = true
416
+ draft.content = x.data
417
+ }),
418
+ )
419
+ })
420
+ .catch((e) => {
421
+ if (scope() !== directory) return
422
+ showToast({
423
+ variant: "error",
424
+ title: language.t("toast.file.loadFailed.title"),
425
+ description: e.message,
426
+ })
427
+ })
428
+ }
429
+
430
+ const fetch = async (path: string) => {
431
+ const relativePath = relative(path)
432
+ const parent = relativePath.split("/").slice(0, -1).join("/")
433
+ if (parent) {
434
+ await list(parent)
435
+ }
436
+ }
437
+
438
+ const init = async (path: string) => {
439
+ const relativePath = relative(path)
440
+ if (!store.node[relativePath]) await fetch(path)
441
+ if (store.node[relativePath]?.loaded) return
442
+ return load(relativePath)
443
+ }
444
+
445
+ const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
446
+ const relativePath = relative(path)
447
+ if (!store.node[relativePath]) await fetch(path)
448
+ // setStore("opened", (x) => {
449
+ // if (x.includes(relativePath)) return x
450
+ // return [
451
+ // ...opened()
452
+ // .filter((x) => x.pinned)
453
+ // .map((x) => x.path),
454
+ // relativePath,
455
+ // ]
456
+ // })
457
+ // setStore("active", relativePath)
458
+ // context.addActive()
459
+ if (options?.pinned) setStore("node", path, "pinned", true)
460
+ if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
461
+ if (store.node[relativePath]?.loaded) return
462
+ return load(relativePath)
463
+ }
464
+
465
+ const list = async (path: string) => {
466
+ const directory = scope()
467
+ const client = sdk.client
468
+ return client.file
469
+ .list({ path: path + "/" })
470
+ .then((x) => {
471
+ if (scope() !== directory) return
472
+ setStore(
473
+ "node",
474
+ produce((draft) => {
475
+ x.data!.forEach((node) => {
476
+ if (node.path in draft) return
477
+ draft[node.path] = node
478
+ })
479
+ }),
480
+ )
481
+ })
482
+ .catch(() => {})
483
+ }
484
+
485
+ const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
486
+ const searchFilesAndDirectories = (query: string) =>
487
+ sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
488
+
489
+ const unsub = sdk.event.listen((e) => {
490
+ const event = e.details
491
+ switch (event.type) {
492
+ case "file.watcher.updated":
493
+ const relativePath = relative(event.properties.file)
494
+ if (relativePath.startsWith(".git/")) return
495
+ if (store.node[relativePath]) load(relativePath)
496
+ break
497
+ }
498
+ })
499
+ onCleanup(unsub)
500
+
501
+ return {
502
+ node: async (path: string) => {
503
+ if (!store.node[path] || !store.node[path].loaded) {
504
+ await init(path)
505
+ }
506
+ return store.node[path]
507
+ },
508
+ update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
509
+ open,
510
+ load,
511
+ init,
512
+ expand(path: string) {
513
+ setStore("node", path, "expanded", true)
514
+ if (store.node[path]?.loaded) return
515
+ setStore("node", path, "loaded", true)
516
+ list(path)
517
+ },
518
+ collapse(path: string) {
519
+ setStore("node", path, "expanded", false)
520
+ },
521
+ select(path: string, selection: TextSelection | undefined) {
522
+ setStore("node", path, "selection", selection)
523
+ },
524
+ scroll(path: string, scrollTop: number) {
525
+ setStore("node", path, "scrollTop", scrollTop)
526
+ },
527
+ view(path: string): View {
528
+ const n = store.node[path]
529
+ return n && n.view ? n.view : "raw"
530
+ },
531
+ setView(path: string, view: View) {
532
+ setStore("node", path, "view", view)
533
+ },
534
+ unfold(path: string, key: string) {
535
+ setStore("node", path, "folded", (xs) => {
536
+ const a = xs ?? []
537
+ if (a.includes(key)) return a
538
+ return [...a, key]
539
+ })
540
+ },
541
+ fold(path: string, key: string) {
542
+ setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key))
543
+ },
544
+ folded(path: string) {
545
+ const n = store.node[path]
546
+ return n && n.folded ? n.folded : []
547
+ },
548
+ changeIndex(path: string) {
549
+ return store.node[path]?.selectedChange
550
+ },
551
+ setChangeIndex(path: string, index: number | undefined) {
552
+ setStore("node", path, "selectedChange", index)
553
+ },
554
+ // changes,
555
+ // changed,
556
+ children(path: string) {
557
+ return Object.values(store.node).filter(
558
+ (x) =>
559
+ x.path.startsWith(path) &&
560
+ x.path !== path &&
561
+ !x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
562
+ )
563
+ },
564
+ searchFiles,
565
+ searchFilesAndDirectories,
566
+ relative,
567
+ }
568
+ })()
569
+
570
+ const result = {
571
+ slug: createMemo(() => base64Encode(sdk.directory)),
572
+ model,
573
+ agent,
574
+ file,
575
+ }
576
+ return result
577
+ },
578
+ })