@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,437 @@
|
|
|
1
|
+
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
|
2
|
+
import { Button } from "@jonsoc/ui/button"
|
|
3
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
4
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
5
|
+
import { TextField } from "@jonsoc/ui/text-field"
|
|
6
|
+
import { showToast } from "@jonsoc/ui/toast"
|
|
7
|
+
import fuzzysort from "fuzzysort"
|
|
8
|
+
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
|
9
|
+
import { useLanguage } from "@/context/language"
|
|
10
|
+
import { useSettings } from "@/context/settings"
|
|
11
|
+
|
|
12
|
+
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
|
13
|
+
const PALETTE_ID = "command.palette"
|
|
14
|
+
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
|
15
|
+
|
|
16
|
+
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
|
17
|
+
|
|
18
|
+
type KeybindMeta = {
|
|
19
|
+
title: string
|
|
20
|
+
group: KeybindGroup
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"]
|
|
24
|
+
|
|
25
|
+
type GroupKey =
|
|
26
|
+
| "settings.shortcuts.group.general"
|
|
27
|
+
| "settings.shortcuts.group.session"
|
|
28
|
+
| "settings.shortcuts.group.navigation"
|
|
29
|
+
| "settings.shortcuts.group.modelAndAgent"
|
|
30
|
+
| "settings.shortcuts.group.terminal"
|
|
31
|
+
| "settings.shortcuts.group.prompt"
|
|
32
|
+
|
|
33
|
+
const groupKey: Record<KeybindGroup, GroupKey> = {
|
|
34
|
+
General: "settings.shortcuts.group.general",
|
|
35
|
+
Session: "settings.shortcuts.group.session",
|
|
36
|
+
Navigation: "settings.shortcuts.group.navigation",
|
|
37
|
+
"Model and agent": "settings.shortcuts.group.modelAndAgent",
|
|
38
|
+
Terminal: "settings.shortcuts.group.terminal",
|
|
39
|
+
Prompt: "settings.shortcuts.group.prompt",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function groupFor(id: string): KeybindGroup {
|
|
43
|
+
if (id === PALETTE_ID) return "General"
|
|
44
|
+
if (id.startsWith("terminal.")) return "Terminal"
|
|
45
|
+
if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent"
|
|
46
|
+
if (id.startsWith("file.")) return "Navigation"
|
|
47
|
+
if (id.startsWith("prompt.")) return "Prompt"
|
|
48
|
+
if (
|
|
49
|
+
id.startsWith("session.") ||
|
|
50
|
+
id.startsWith("message.") ||
|
|
51
|
+
id.startsWith("permissions.") ||
|
|
52
|
+
id.startsWith("steps.") ||
|
|
53
|
+
id.startsWith("review.")
|
|
54
|
+
)
|
|
55
|
+
return "Session"
|
|
56
|
+
|
|
57
|
+
return "General"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isModifier(key: string) {
|
|
61
|
+
return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeKey(key: string) {
|
|
65
|
+
if (key === ",") return "comma"
|
|
66
|
+
if (key === "+") return "plus"
|
|
67
|
+
if (key === " ") return "space"
|
|
68
|
+
return key.toLowerCase()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function recordKeybind(event: KeyboardEvent) {
|
|
72
|
+
if (isModifier(event.key)) return
|
|
73
|
+
|
|
74
|
+
const parts: string[] = []
|
|
75
|
+
|
|
76
|
+
const mod = IS_MAC ? event.metaKey : event.ctrlKey
|
|
77
|
+
if (mod) parts.push("mod")
|
|
78
|
+
|
|
79
|
+
if (IS_MAC && event.ctrlKey) parts.push("ctrl")
|
|
80
|
+
if (!IS_MAC && event.metaKey) parts.push("meta")
|
|
81
|
+
if (event.altKey) parts.push("alt")
|
|
82
|
+
if (event.shiftKey) parts.push("shift")
|
|
83
|
+
|
|
84
|
+
const key = normalizeKey(event.key)
|
|
85
|
+
if (!key) return
|
|
86
|
+
parts.push(key)
|
|
87
|
+
|
|
88
|
+
return parts.join("+")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function signatures(config: string | undefined) {
|
|
92
|
+
if (!config) return []
|
|
93
|
+
const sigs: string[] = []
|
|
94
|
+
|
|
95
|
+
for (const kb of parseKeybind(config)) {
|
|
96
|
+
const parts: string[] = []
|
|
97
|
+
if (kb.ctrl) parts.push("ctrl")
|
|
98
|
+
if (kb.alt) parts.push("alt")
|
|
99
|
+
if (kb.shift) parts.push("shift")
|
|
100
|
+
if (kb.meta) parts.push("meta")
|
|
101
|
+
if (kb.key) parts.push(kb.key)
|
|
102
|
+
if (parts.length === 0) continue
|
|
103
|
+
sigs.push(parts.join("+"))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return sigs
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const SettingsKeybinds: Component = () => {
|
|
110
|
+
const command = useCommand()
|
|
111
|
+
const language = useLanguage()
|
|
112
|
+
const settings = useSettings()
|
|
113
|
+
|
|
114
|
+
const [active, setActive] = createSignal<string | null>(null)
|
|
115
|
+
const [filter, setFilter] = createSignal("")
|
|
116
|
+
|
|
117
|
+
const stop = () => {
|
|
118
|
+
if (!active()) return
|
|
119
|
+
setActive(null)
|
|
120
|
+
command.keybinds(true)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const start = (id: string) => {
|
|
124
|
+
if (active() === id) {
|
|
125
|
+
stop()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (active()) stop()
|
|
130
|
+
|
|
131
|
+
setActive(id)
|
|
132
|
+
command.keybinds(false)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hasOverrides = createMemo(() => {
|
|
136
|
+
const keybinds = settings.current.keybinds as Record<string, string | undefined> | undefined
|
|
137
|
+
if (!keybinds) return false
|
|
138
|
+
return Object.values(keybinds).some((x) => typeof x === "string")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const resetAll = () => {
|
|
142
|
+
stop()
|
|
143
|
+
settings.keybinds.resetAll()
|
|
144
|
+
showToast({
|
|
145
|
+
title: language.t("settings.shortcuts.reset.toast.title"),
|
|
146
|
+
description: language.t("settings.shortcuts.reset.toast.description"),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const list = createMemo(() => {
|
|
151
|
+
language.locale()
|
|
152
|
+
const out = new Map<string, KeybindMeta>()
|
|
153
|
+
out.set(PALETTE_ID, { title: language.t("command.palette"), group: "General" })
|
|
154
|
+
|
|
155
|
+
for (const opt of command.catalog) {
|
|
156
|
+
if (opt.id.startsWith("suggested.")) continue
|
|
157
|
+
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const opt of command.options) {
|
|
161
|
+
if (opt.id.startsWith("suggested.")) continue
|
|
162
|
+
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const keybinds = settings.current.keybinds as Record<string, string | undefined> | undefined
|
|
166
|
+
if (keybinds) {
|
|
167
|
+
for (const [id, value] of Object.entries(keybinds)) {
|
|
168
|
+
if (typeof value !== "string") continue
|
|
169
|
+
if (out.has(id)) continue
|
|
170
|
+
out.set(id, { title: id, group: groupFor(id) })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return out
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const title = (id: string) => list().get(id)?.title ?? ""
|
|
178
|
+
|
|
179
|
+
const grouped = createMemo(() => {
|
|
180
|
+
const map = list()
|
|
181
|
+
const out = new Map<KeybindGroup, string[]>()
|
|
182
|
+
|
|
183
|
+
for (const group of GROUPS) out.set(group, [])
|
|
184
|
+
|
|
185
|
+
for (const [id, item] of map) {
|
|
186
|
+
const ids = out.get(item.group)
|
|
187
|
+
if (!ids) continue
|
|
188
|
+
ids.push(id)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const group of GROUPS) {
|
|
192
|
+
const ids = out.get(group)
|
|
193
|
+
if (!ids) continue
|
|
194
|
+
|
|
195
|
+
ids.sort((a, b) => {
|
|
196
|
+
const at = map.get(a)?.title ?? ""
|
|
197
|
+
const bt = map.get(b)?.title ?? ""
|
|
198
|
+
return at.localeCompare(bt)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return out
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const filtered = createMemo(() => {
|
|
206
|
+
const query = filter().toLowerCase().trim()
|
|
207
|
+
if (!query) return grouped()
|
|
208
|
+
|
|
209
|
+
const map = list()
|
|
210
|
+
const out = new Map<KeybindGroup, string[]>()
|
|
211
|
+
|
|
212
|
+
for (const group of GROUPS) out.set(group, [])
|
|
213
|
+
|
|
214
|
+
const items = Array.from(map.entries()).map(([id, meta]) => ({
|
|
215
|
+
id,
|
|
216
|
+
title: meta.title,
|
|
217
|
+
group: meta.group,
|
|
218
|
+
keybind: command.keybind(id) || "",
|
|
219
|
+
}))
|
|
220
|
+
|
|
221
|
+
const results = fuzzysort.go(query, items, {
|
|
222
|
+
keys: ["title", "keybind"],
|
|
223
|
+
threshold: -10000,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
for (const result of results) {
|
|
227
|
+
const item = result.obj
|
|
228
|
+
const ids = out.get(item.group)
|
|
229
|
+
if (!ids) continue
|
|
230
|
+
ids.push(item.id)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return out
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const hasResults = createMemo(() => {
|
|
237
|
+
for (const group of GROUPS) {
|
|
238
|
+
const ids = filtered().get(group) ?? []
|
|
239
|
+
if (ids.length > 0) return true
|
|
240
|
+
}
|
|
241
|
+
return false
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const used = createMemo(() => {
|
|
245
|
+
const map = new Map<string, { id: string; title: string }[]>()
|
|
246
|
+
|
|
247
|
+
const add = (key: string, value: { id: string; title: string }) => {
|
|
248
|
+
const list = map.get(key)
|
|
249
|
+
if (!list) {
|
|
250
|
+
map.set(key, [value])
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
list.push(value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
|
257
|
+
for (const sig of signatures(palette)) {
|
|
258
|
+
add(sig, { id: PALETTE_ID, title: title(PALETTE_ID) })
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const valueFor = (id: string) => {
|
|
262
|
+
const custom = settings.keybinds.get(id)
|
|
263
|
+
if (typeof custom === "string") return custom
|
|
264
|
+
|
|
265
|
+
const live = command.options.find((x) => x.id === id)
|
|
266
|
+
if (live?.keybind) return live.keybind
|
|
267
|
+
|
|
268
|
+
const meta = command.catalog.find((x) => x.id === id)
|
|
269
|
+
return meta?.keybind
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const id of list().keys()) {
|
|
273
|
+
if (id === PALETTE_ID) continue
|
|
274
|
+
for (const sig of signatures(valueFor(id))) {
|
|
275
|
+
add(sig, { id, title: title(id) })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return map
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const setKeybind = (id: string, keybind: string) => {
|
|
283
|
+
settings.keybinds.set(id, keybind)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
onMount(() => {
|
|
287
|
+
const handle = (event: KeyboardEvent) => {
|
|
288
|
+
const id = active()
|
|
289
|
+
if (!id) return
|
|
290
|
+
|
|
291
|
+
event.preventDefault()
|
|
292
|
+
event.stopPropagation()
|
|
293
|
+
event.stopImmediatePropagation()
|
|
294
|
+
|
|
295
|
+
if (event.key === "Escape") {
|
|
296
|
+
stop()
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const clear =
|
|
301
|
+
(event.key === "Backspace" || event.key === "Delete") &&
|
|
302
|
+
!event.ctrlKey &&
|
|
303
|
+
!event.metaKey &&
|
|
304
|
+
!event.altKey &&
|
|
305
|
+
!event.shiftKey
|
|
306
|
+
if (clear) {
|
|
307
|
+
setKeybind(id, "none")
|
|
308
|
+
stop()
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const next = recordKeybind(event)
|
|
313
|
+
if (!next) return
|
|
314
|
+
|
|
315
|
+
const map = used()
|
|
316
|
+
const conflicts = new Map<string, string>()
|
|
317
|
+
|
|
318
|
+
for (const sig of signatures(next)) {
|
|
319
|
+
const list = map.get(sig) ?? []
|
|
320
|
+
for (const item of list) {
|
|
321
|
+
if (item.id === id) continue
|
|
322
|
+
conflicts.set(item.id, item.title)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (conflicts.size > 0) {
|
|
327
|
+
showToast({
|
|
328
|
+
title: language.t("settings.shortcuts.conflict.title"),
|
|
329
|
+
description: language.t("settings.shortcuts.conflict.description", {
|
|
330
|
+
keybind: formatKeybind(next),
|
|
331
|
+
titles: [...conflicts.values()].join(", "),
|
|
332
|
+
}),
|
|
333
|
+
})
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setKeybind(id, next)
|
|
338
|
+
stop()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
document.addEventListener("keydown", handle, true)
|
|
342
|
+
onCleanup(() => {
|
|
343
|
+
document.removeEventListener("keydown", handle, true)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
onCleanup(() => {
|
|
348
|
+
if (active()) command.keybinds(true)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
|
353
|
+
<div
|
|
354
|
+
class="sticky top-0 z-10"
|
|
355
|
+
style={{
|
|
356
|
+
background:
|
|
357
|
+
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
|
361
|
+
<div class="flex items-center justify-between gap-4">
|
|
362
|
+
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
|
363
|
+
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
|
364
|
+
{language.t("settings.shortcuts.reset.button")}
|
|
365
|
+
</Button>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
|
369
|
+
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
|
370
|
+
<TextField
|
|
371
|
+
variant="ghost"
|
|
372
|
+
type="text"
|
|
373
|
+
value={filter()}
|
|
374
|
+
onChange={setFilter}
|
|
375
|
+
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
|
376
|
+
spellcheck={false}
|
|
377
|
+
autocorrect="off"
|
|
378
|
+
autocomplete="off"
|
|
379
|
+
autocapitalize="off"
|
|
380
|
+
class="flex-1"
|
|
381
|
+
/>
|
|
382
|
+
<Show when={filter()}>
|
|
383
|
+
<IconButton icon="circle-x" variant="ghost" onClick={() => setFilter("")} />
|
|
384
|
+
</Show>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div class="flex flex-col gap-8 max-w-[720px]">
|
|
390
|
+
<For each={GROUPS}>
|
|
391
|
+
{(group) => (
|
|
392
|
+
<Show when={(filtered().get(group) ?? []).length > 0}>
|
|
393
|
+
<div class="flex flex-col gap-1">
|
|
394
|
+
<h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
|
|
395
|
+
<div class="bg-surface-raised-base px-4 rounded-lg">
|
|
396
|
+
<For each={filtered().get(group) ?? []}>
|
|
397
|
+
{(id) => (
|
|
398
|
+
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
|
399
|
+
<span class="text-14-regular text-text-strong">{title(id)}</span>
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
classList={{
|
|
403
|
+
"h-8 px-3 rounded-md text-12-regular": true,
|
|
404
|
+
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
|
405
|
+
active() !== id,
|
|
406
|
+
"border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id,
|
|
407
|
+
}}
|
|
408
|
+
onClick={() => start(id)}
|
|
409
|
+
>
|
|
410
|
+
<Show
|
|
411
|
+
when={active() === id}
|
|
412
|
+
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
|
413
|
+
>
|
|
414
|
+
{language.t("settings.shortcuts.pressKeys")}
|
|
415
|
+
</Show>
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</For>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</Show>
|
|
423
|
+
)}
|
|
424
|
+
</For>
|
|
425
|
+
|
|
426
|
+
<Show when={filter() && !hasResults()}>
|
|
427
|
+
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
428
|
+
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
|
|
429
|
+
<Show when={filter()}>
|
|
430
|
+
<span class="text-14-regular text-text-strong mt-1">"{filter()}"</span>
|
|
431
|
+
</Show>
|
|
432
|
+
</div>
|
|
433
|
+
</Show>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Component } from "solid-js"
|
|
2
|
+
import { useLanguage } from "@/context/language"
|
|
3
|
+
|
|
4
|
+
export const SettingsMcp: Component = () => {
|
|
5
|
+
const language = useLanguage()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div class="flex flex-col h-full overflow-y-auto">
|
|
9
|
+
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
|
10
|
+
<h2 class="text-16-medium text-text-strong">{language.t("settings.mcp.title")}</h2>
|
|
11
|
+
<p class="text-14-regular text-text-weak">{language.t("settings.mcp.description")}</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Component } from "solid-js"
|
|
2
|
+
import { useLanguage } from "@/context/language"
|
|
3
|
+
|
|
4
|
+
export const SettingsModels: Component = () => {
|
|
5
|
+
const language = useLanguage()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div class="flex flex-col h-full overflow-y-auto">
|
|
9
|
+
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
|
10
|
+
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
|
|
11
|
+
<p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Select } from "@jonsoc/ui/select"
|
|
2
|
+
import { showToast } from "@jonsoc/ui/toast"
|
|
3
|
+
import { Component, For, createMemo, type JSX } from "solid-js"
|
|
4
|
+
import { useGlobalSync } from "@/context/global-sync"
|
|
5
|
+
import { useLanguage } from "@/context/language"
|
|
6
|
+
|
|
7
|
+
type PermissionAction = "allow" | "ask" | "deny"
|
|
8
|
+
|
|
9
|
+
type PermissionObject = Record<string, PermissionAction>
|
|
10
|
+
type PermissionValue = PermissionAction | PermissionObject | string[] | undefined
|
|
11
|
+
type PermissionMap = Record<string, PermissionValue>
|
|
12
|
+
|
|
13
|
+
type PermissionItem = {
|
|
14
|
+
id: string
|
|
15
|
+
title: string
|
|
16
|
+
description: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ACTIONS = [
|
|
20
|
+
{ value: "allow", label: "settings.permissions.action.allow" },
|
|
21
|
+
{ value: "ask", label: "settings.permissions.action.ask" },
|
|
22
|
+
{ value: "deny", label: "settings.permissions.action.deny" },
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
const ITEMS = [
|
|
26
|
+
{
|
|
27
|
+
id: "read",
|
|
28
|
+
title: "settings.permissions.tool.read.title",
|
|
29
|
+
description: "settings.permissions.tool.read.description",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "edit",
|
|
33
|
+
title: "settings.permissions.tool.edit.title",
|
|
34
|
+
description: "settings.permissions.tool.edit.description",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "glob",
|
|
38
|
+
title: "settings.permissions.tool.glob.title",
|
|
39
|
+
description: "settings.permissions.tool.glob.description",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "grep",
|
|
43
|
+
title: "settings.permissions.tool.grep.title",
|
|
44
|
+
description: "settings.permissions.tool.grep.description",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "list",
|
|
48
|
+
title: "settings.permissions.tool.list.title",
|
|
49
|
+
description: "settings.permissions.tool.list.description",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "bash",
|
|
53
|
+
title: "settings.permissions.tool.bash.title",
|
|
54
|
+
description: "settings.permissions.tool.bash.description",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "task",
|
|
58
|
+
title: "settings.permissions.tool.task.title",
|
|
59
|
+
description: "settings.permissions.tool.task.description",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "skill",
|
|
63
|
+
title: "settings.permissions.tool.skill.title",
|
|
64
|
+
description: "settings.permissions.tool.skill.description",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "lsp",
|
|
68
|
+
title: "settings.permissions.tool.lsp.title",
|
|
69
|
+
description: "settings.permissions.tool.lsp.description",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "todoread",
|
|
73
|
+
title: "settings.permissions.tool.todoread.title",
|
|
74
|
+
description: "settings.permissions.tool.todoread.description",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "todowrite",
|
|
78
|
+
title: "settings.permissions.tool.todowrite.title",
|
|
79
|
+
description: "settings.permissions.tool.todowrite.description",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "webfetch",
|
|
83
|
+
title: "settings.permissions.tool.webfetch.title",
|
|
84
|
+
description: "settings.permissions.tool.webfetch.description",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "websearch",
|
|
88
|
+
title: "settings.permissions.tool.websearch.title",
|
|
89
|
+
description: "settings.permissions.tool.websearch.description",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "codesearch",
|
|
93
|
+
title: "settings.permissions.tool.codesearch.title",
|
|
94
|
+
description: "settings.permissions.tool.codesearch.description",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "external_directory",
|
|
98
|
+
title: "settings.permissions.tool.external_directory.title",
|
|
99
|
+
description: "settings.permissions.tool.external_directory.description",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "doom_loop",
|
|
103
|
+
title: "settings.permissions.tool.doom_loop.title",
|
|
104
|
+
description: "settings.permissions.tool.doom_loop.description",
|
|
105
|
+
},
|
|
106
|
+
] as const
|
|
107
|
+
|
|
108
|
+
const VALID_ACTIONS = new Set<PermissionAction>(["allow", "ask", "deny"])
|
|
109
|
+
|
|
110
|
+
function toMap(value: unknown): PermissionMap {
|
|
111
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap
|
|
112
|
+
|
|
113
|
+
const action = getAction(value)
|
|
114
|
+
if (action) return { "*": action }
|
|
115
|
+
|
|
116
|
+
return {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getAction(value: unknown): PermissionAction | undefined {
|
|
120
|
+
if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getRuleDefault(value: unknown): PermissionAction | undefined {
|
|
125
|
+
const action = getAction(value)
|
|
126
|
+
if (action) return action
|
|
127
|
+
|
|
128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return
|
|
129
|
+
|
|
130
|
+
return getAction((value as Record<string, unknown>)["*"])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const SettingsPermissions: Component = () => {
|
|
134
|
+
const globalSync = useGlobalSync()
|
|
135
|
+
const language = useLanguage()
|
|
136
|
+
|
|
137
|
+
const actions = createMemo(
|
|
138
|
+
(): Array<{ value: PermissionAction; label: string }> =>
|
|
139
|
+
ACTIONS.map((action) => ({
|
|
140
|
+
value: action.value,
|
|
141
|
+
label: language.t(action.label),
|
|
142
|
+
})),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const permission = createMemo(() => {
|
|
146
|
+
return toMap(globalSync.data.config.permission)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const actionFor = (id: string): PermissionAction => {
|
|
150
|
+
const value = permission()[id]
|
|
151
|
+
const direct = getRuleDefault(value)
|
|
152
|
+
if (direct) return direct
|
|
153
|
+
|
|
154
|
+
const wildcard = getRuleDefault(permission()["*"])
|
|
155
|
+
if (wildcard) return wildcard
|
|
156
|
+
|
|
157
|
+
return "allow"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const setPermission = async (id: string, action: PermissionAction) => {
|
|
161
|
+
const before = globalSync.data.config.permission
|
|
162
|
+
const map = toMap(before)
|
|
163
|
+
const existing = map[id]
|
|
164
|
+
|
|
165
|
+
const nextValue =
|
|
166
|
+
existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action
|
|
167
|
+
|
|
168
|
+
globalSync.set("config", "permission", { ...map, [id]: nextValue })
|
|
169
|
+
globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => {
|
|
170
|
+
globalSync.set("config", "permission", before)
|
|
171
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
172
|
+
showToast({ title: language.t("settings.permissions.toast.updateFailed.title"), description: message })
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
|
|
178
|
+
<div
|
|
179
|
+
class="sticky top-0 z-10"
|
|
180
|
+
style={{
|
|
181
|
+
background:
|
|
182
|
+
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<div class="flex flex-col gap-1 p-8 max-w-[720px]">
|
|
186
|
+
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
|
|
187
|
+
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
|
|
192
|
+
<div class="flex flex-col gap-2">
|
|
193
|
+
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
|
|
194
|
+
<div class="border border-border-weak-base rounded-lg overflow-hidden">
|
|
195
|
+
<For each={ITEMS}>
|
|
196
|
+
{(item) => (
|
|
197
|
+
<SettingsRow title={language.t(item.title)} description={language.t(item.description)}>
|
|
198
|
+
<Select
|
|
199
|
+
options={actions()}
|
|
200
|
+
current={actions().find((o) => o.value === actionFor(item.id))}
|
|
201
|
+
value={(o) => o.value}
|
|
202
|
+
label={(o) => o.label}
|
|
203
|
+
onSelect={(option) => option && setPermission(item.id, option.value)}
|
|
204
|
+
variant="secondary"
|
|
205
|
+
size="small"
|
|
206
|
+
triggerVariant="settings"
|
|
207
|
+
/>
|
|
208
|
+
</SettingsRow>
|
|
209
|
+
)}
|
|
210
|
+
</For>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface SettingsRowProps {
|
|
219
|
+
title: string
|
|
220
|
+
description: string
|
|
221
|
+
children: JSX.Element
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
|
225
|
+
return (
|
|
226
|
+
<div class="flex items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
|
|
227
|
+
<div class="flex flex-col gap-0.5">
|
|
228
|
+
<span class="text-14-medium text-text-strong">{props.title}</span>
|
|
229
|
+
<span class="text-12-regular text-text-weak">{props.description}</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="flex-shrink-0">{props.children}</div>
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|