@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,308 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { createSimpleContext } from "@jonsoc/ui/context"
|
|
4
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
5
|
+
import { useLanguage } from "@/context/language"
|
|
6
|
+
import { useSettings } from "@/context/settings"
|
|
7
|
+
import { Persist, persisted } from "@/utils/persist"
|
|
8
|
+
|
|
9
|
+
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
|
10
|
+
|
|
11
|
+
const PALETTE_ID = "command.palette"
|
|
12
|
+
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
|
13
|
+
const SUGGESTED_PREFIX = "suggested."
|
|
14
|
+
|
|
15
|
+
function actionId(id: string) {
|
|
16
|
+
if (!id.startsWith(SUGGESTED_PREFIX)) return id
|
|
17
|
+
return id.slice(SUGGESTED_PREFIX.length)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeKey(key: string) {
|
|
21
|
+
if (key === ",") return "comma"
|
|
22
|
+
if (key === "+") return "plus"
|
|
23
|
+
if (key === " ") return "space"
|
|
24
|
+
return key.toLowerCase()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type KeybindConfig = string
|
|
28
|
+
|
|
29
|
+
export interface Keybind {
|
|
30
|
+
key: string
|
|
31
|
+
ctrl: boolean
|
|
32
|
+
meta: boolean
|
|
33
|
+
shift: boolean
|
|
34
|
+
alt: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CommandOption {
|
|
38
|
+
id: string
|
|
39
|
+
title: string
|
|
40
|
+
description?: string
|
|
41
|
+
category?: string
|
|
42
|
+
keybind?: KeybindConfig
|
|
43
|
+
slash?: string
|
|
44
|
+
suggested?: boolean
|
|
45
|
+
disabled?: boolean
|
|
46
|
+
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
|
47
|
+
onHighlight?: () => (() => void) | void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type CommandCatalogItem = {
|
|
51
|
+
title: string
|
|
52
|
+
description?: string
|
|
53
|
+
category?: string
|
|
54
|
+
keybind?: KeybindConfig
|
|
55
|
+
slash?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseKeybind(config: string): Keybind[] {
|
|
59
|
+
if (!config || config === "none") return []
|
|
60
|
+
|
|
61
|
+
return config.split(",").map((combo) => {
|
|
62
|
+
const parts = combo.trim().toLowerCase().split("+")
|
|
63
|
+
const keybind: Keybind = {
|
|
64
|
+
key: "",
|
|
65
|
+
ctrl: false,
|
|
66
|
+
meta: false,
|
|
67
|
+
shift: false,
|
|
68
|
+
alt: false,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
switch (part) {
|
|
73
|
+
case "ctrl":
|
|
74
|
+
case "control":
|
|
75
|
+
keybind.ctrl = true
|
|
76
|
+
break
|
|
77
|
+
case "meta":
|
|
78
|
+
case "cmd":
|
|
79
|
+
case "command":
|
|
80
|
+
keybind.meta = true
|
|
81
|
+
break
|
|
82
|
+
case "mod":
|
|
83
|
+
if (IS_MAC) keybind.meta = true
|
|
84
|
+
else keybind.ctrl = true
|
|
85
|
+
break
|
|
86
|
+
case "alt":
|
|
87
|
+
case "option":
|
|
88
|
+
keybind.alt = true
|
|
89
|
+
break
|
|
90
|
+
case "shift":
|
|
91
|
+
keybind.shift = true
|
|
92
|
+
break
|
|
93
|
+
default:
|
|
94
|
+
keybind.key = part
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return keybind
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean {
|
|
104
|
+
const eventKey = normalizeKey(event.key)
|
|
105
|
+
|
|
106
|
+
for (const kb of keybinds) {
|
|
107
|
+
const keyMatch = kb.key === eventKey
|
|
108
|
+
const ctrlMatch = kb.ctrl === (event.ctrlKey || false)
|
|
109
|
+
const metaMatch = kb.meta === (event.metaKey || false)
|
|
110
|
+
const shiftMatch = kb.shift === (event.shiftKey || false)
|
|
111
|
+
const altMatch = kb.alt === (event.altKey || false)
|
|
112
|
+
|
|
113
|
+
if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) {
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatKeybind(config: string): string {
|
|
122
|
+
if (!config || config === "none") return ""
|
|
123
|
+
|
|
124
|
+
const keybinds = parseKeybind(config)
|
|
125
|
+
if (keybinds.length === 0) return ""
|
|
126
|
+
|
|
127
|
+
const kb = keybinds[0]
|
|
128
|
+
const parts: string[] = []
|
|
129
|
+
|
|
130
|
+
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
|
|
131
|
+
if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
|
|
132
|
+
if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
|
|
133
|
+
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
|
134
|
+
|
|
135
|
+
if (kb.key) {
|
|
136
|
+
const keys: Record<string, string> = {
|
|
137
|
+
arrowup: "↑",
|
|
138
|
+
arrowdown: "↓",
|
|
139
|
+
arrowleft: "←",
|
|
140
|
+
arrowright: "→",
|
|
141
|
+
comma: ",",
|
|
142
|
+
plus: "+",
|
|
143
|
+
space: "Space",
|
|
144
|
+
}
|
|
145
|
+
const key = kb.key.toLowerCase()
|
|
146
|
+
const displayKey = keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1))
|
|
147
|
+
parts.push(displayKey)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return IS_MAC ? parts.join("") : parts.join("+")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
|
154
|
+
name: "Command",
|
|
155
|
+
init: () => {
|
|
156
|
+
const dialog = useDialog()
|
|
157
|
+
const settings = useSettings()
|
|
158
|
+
const language = useLanguage()
|
|
159
|
+
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
|
160
|
+
const [suspendCount, setSuspendCount] = createSignal(0)
|
|
161
|
+
|
|
162
|
+
const [catalog, setCatalog, _, catalogReady] = persisted(
|
|
163
|
+
Persist.global("command.catalog.v1"),
|
|
164
|
+
createStore<Record<string, CommandCatalogItem>>({}),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
const bind = (id: string, def: KeybindConfig | undefined) => {
|
|
168
|
+
const custom = settings.keybinds.get(actionId(id))
|
|
169
|
+
const config = custom ?? def
|
|
170
|
+
if (!config || config === "none") return
|
|
171
|
+
return config
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const registered = createMemo(() => {
|
|
175
|
+
const seen = new Set<string>()
|
|
176
|
+
const all: CommandOption[] = []
|
|
177
|
+
|
|
178
|
+
for (const reg of registrations()) {
|
|
179
|
+
for (const opt of reg()) {
|
|
180
|
+
if (seen.has(opt.id)) continue
|
|
181
|
+
seen.add(opt.id)
|
|
182
|
+
all.push(opt)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return all
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
createEffect(() => {
|
|
190
|
+
if (!catalogReady()) return
|
|
191
|
+
|
|
192
|
+
for (const opt of registered()) {
|
|
193
|
+
const id = actionId(opt.id)
|
|
194
|
+
setCatalog(id, {
|
|
195
|
+
title: opt.title,
|
|
196
|
+
description: opt.description,
|
|
197
|
+
category: opt.category,
|
|
198
|
+
keybind: opt.keybind,
|
|
199
|
+
slash: opt.slash,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const catalogOptions = createMemo(() => Object.entries(catalog).map(([id, meta]) => ({ id, ...meta })))
|
|
205
|
+
|
|
206
|
+
const options = createMemo(() => {
|
|
207
|
+
const resolved = registered().map((opt) => ({
|
|
208
|
+
...opt,
|
|
209
|
+
keybind: bind(opt.id, opt.keybind),
|
|
210
|
+
}))
|
|
211
|
+
|
|
212
|
+
const suggested = resolved.filter((x) => x.suggested && !x.disabled)
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
...suggested.map((x) => ({
|
|
216
|
+
...x,
|
|
217
|
+
id: SUGGESTED_PREFIX + x.id,
|
|
218
|
+
category: language.t("command.category.suggested"),
|
|
219
|
+
})),
|
|
220
|
+
...resolved,
|
|
221
|
+
]
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const suspended = () => suspendCount() > 0
|
|
225
|
+
|
|
226
|
+
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
|
|
227
|
+
for (const option of options()) {
|
|
228
|
+
if (option.id === id || option.id === "suggested." + id) {
|
|
229
|
+
option.onSelect?.(source)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const showPalette = () => {
|
|
236
|
+
run("file.open", "palette")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
240
|
+
if (suspended() || dialog.active) return
|
|
241
|
+
|
|
242
|
+
const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
|
243
|
+
if (matchKeybind(paletteKeybinds, event)) {
|
|
244
|
+
event.preventDefault()
|
|
245
|
+
showPalette()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const option of options()) {
|
|
250
|
+
if (option.disabled) continue
|
|
251
|
+
if (!option.keybind) continue
|
|
252
|
+
|
|
253
|
+
const keybinds = parseKeybind(option.keybind)
|
|
254
|
+
if (matchKeybind(keybinds, event)) {
|
|
255
|
+
event.preventDefault()
|
|
256
|
+
option.onSelect?.("keybind")
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onMount(() => {
|
|
263
|
+
document.addEventListener("keydown", handleKeyDown)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
onCleanup(() => {
|
|
267
|
+
document.removeEventListener("keydown", handleKeyDown)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
register(cb: () => CommandOption[]) {
|
|
272
|
+
const results = createMemo(cb)
|
|
273
|
+
setRegistrations((arr) => [results, ...arr])
|
|
274
|
+
onCleanup(() => {
|
|
275
|
+
setRegistrations((arr) => arr.filter((x) => x !== results))
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
|
279
|
+
run(id, source)
|
|
280
|
+
},
|
|
281
|
+
keybind(id: string) {
|
|
282
|
+
if (id === PALETTE_ID) {
|
|
283
|
+
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const base = actionId(id)
|
|
287
|
+
const option = options().find((x) => actionId(x.id) === base)
|
|
288
|
+
if (option?.keybind) return formatKeybind(option.keybind)
|
|
289
|
+
|
|
290
|
+
const meta = catalog[base]
|
|
291
|
+
const config = bind(base, meta?.keybind)
|
|
292
|
+
if (!config) return ""
|
|
293
|
+
return formatKeybind(config)
|
|
294
|
+
},
|
|
295
|
+
show: showPalette,
|
|
296
|
+
keybinds(enabled: boolean) {
|
|
297
|
+
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
|
298
|
+
},
|
|
299
|
+
suspended,
|
|
300
|
+
get catalog() {
|
|
301
|
+
return catalogOptions()
|
|
302
|
+
},
|
|
303
|
+
get options() {
|
|
304
|
+
return options()
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { createSimpleContext } from "@jonsoc/ui/context"
|
|
4
|
+
import { useParams } from "@solidjs/router"
|
|
5
|
+
import { Persist, persisted } from "@/utils/persist"
|
|
6
|
+
import type { SelectedLineRange } from "@/context/file"
|
|
7
|
+
|
|
8
|
+
export type LineComment = {
|
|
9
|
+
id: string
|
|
10
|
+
file: string
|
|
11
|
+
selection: SelectedLineRange
|
|
12
|
+
comment: string
|
|
13
|
+
time: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type CommentFocus = { file: string; id: string }
|
|
17
|
+
|
|
18
|
+
const WORKSPACE_KEY = "__workspace__"
|
|
19
|
+
const MAX_COMMENT_SESSIONS = 20
|
|
20
|
+
|
|
21
|
+
type CommentSession = ReturnType<typeof createCommentSession>
|
|
22
|
+
|
|
23
|
+
type CommentCacheEntry = {
|
|
24
|
+
value: CommentSession
|
|
25
|
+
dispose: VoidFunction
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createCommentSession(dir: string, id: string | undefined) {
|
|
29
|
+
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
|
|
30
|
+
|
|
31
|
+
const [store, setStore, _, ready] = persisted(
|
|
32
|
+
Persist.scoped(dir, id, "comments", [legacy]),
|
|
33
|
+
createStore<{
|
|
34
|
+
comments: Record<string, LineComment[]>
|
|
35
|
+
}>({
|
|
36
|
+
comments: {},
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
|
|
41
|
+
|
|
42
|
+
const list = (file: string) => store.comments[file] ?? []
|
|
43
|
+
|
|
44
|
+
const add = (input: Omit<LineComment, "id" | "time">) => {
|
|
45
|
+
const next: LineComment = {
|
|
46
|
+
id: crypto.randomUUID(),
|
|
47
|
+
time: Date.now(),
|
|
48
|
+
...input,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
batch(() => {
|
|
52
|
+
setStore("comments", input.file, (items) => [...(items ?? []), next])
|
|
53
|
+
setFocus({ file: input.file, id: next.id })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return next
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const remove = (file: string, id: string) => {
|
|
60
|
+
setStore("comments", file, (items) => (items ?? []).filter((x) => x.id !== id))
|
|
61
|
+
setFocus((current) => (current?.id === id ? null : current))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const all = createMemo(() => {
|
|
65
|
+
const files = Object.keys(store.comments)
|
|
66
|
+
const items = files.flatMap((file) => store.comments[file] ?? [])
|
|
67
|
+
return items.slice().sort((a, b) => a.time - b.time)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ready,
|
|
72
|
+
list,
|
|
73
|
+
all,
|
|
74
|
+
add,
|
|
75
|
+
remove,
|
|
76
|
+
focus: createMemo(() => focus()),
|
|
77
|
+
setFocus,
|
|
78
|
+
clearFocus: () => setFocus(null),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const { use: useComments, provider: CommentsProvider } = createSimpleContext({
|
|
83
|
+
name: "Comments",
|
|
84
|
+
gate: false,
|
|
85
|
+
init: () => {
|
|
86
|
+
const params = useParams()
|
|
87
|
+
const cache = new Map<string, CommentCacheEntry>()
|
|
88
|
+
|
|
89
|
+
const disposeAll = () => {
|
|
90
|
+
for (const entry of cache.values()) {
|
|
91
|
+
entry.dispose()
|
|
92
|
+
}
|
|
93
|
+
cache.clear()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
onCleanup(disposeAll)
|
|
97
|
+
|
|
98
|
+
const prune = () => {
|
|
99
|
+
while (cache.size > MAX_COMMENT_SESSIONS) {
|
|
100
|
+
const first = cache.keys().next().value
|
|
101
|
+
if (!first) return
|
|
102
|
+
const entry = cache.get(first)
|
|
103
|
+
entry?.dispose()
|
|
104
|
+
cache.delete(first)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const load = (dir: string, id: string | undefined) => {
|
|
109
|
+
const key = `${dir}:${id ?? WORKSPACE_KEY}`
|
|
110
|
+
const existing = cache.get(key)
|
|
111
|
+
if (existing) {
|
|
112
|
+
cache.delete(key)
|
|
113
|
+
cache.set(key, existing)
|
|
114
|
+
return existing.value
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const entry = createRoot((dispose) => ({
|
|
118
|
+
value: createCommentSession(dir, id),
|
|
119
|
+
dispose,
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
cache.set(key, entry)
|
|
123
|
+
prune()
|
|
124
|
+
return entry.value
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const session = createMemo(() => load(params.dir!, params.id))
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ready: () => session().ready(),
|
|
131
|
+
list: (file: string) => session().list(file),
|
|
132
|
+
all: () => session().all(),
|
|
133
|
+
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
|
|
134
|
+
remove: (file: string, id: string) => session().remove(file, id),
|
|
135
|
+
focus: () => session().focus(),
|
|
136
|
+
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
|
|
137
|
+
clearFocus: () => session().clearFocus(),
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
})
|