@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,249 @@
|
|
|
1
|
+
import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
|
|
2
|
+
import { createStore, reconcile } from "solid-js/store"
|
|
3
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
4
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
5
|
+
import { List } from "@jonsoc/ui/list"
|
|
6
|
+
import { TextField } from "@jonsoc/ui/text-field"
|
|
7
|
+
import { Button } from "@jonsoc/ui/button"
|
|
8
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
9
|
+
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
|
|
10
|
+
import { usePlatform } from "@/context/platform"
|
|
11
|
+
import { createOpencodeClient } from "@jonsoc/sdk/v2/client"
|
|
12
|
+
import { useNavigate } from "@solidjs/router"
|
|
13
|
+
import { useLanguage } from "@/context/language"
|
|
14
|
+
|
|
15
|
+
type ServerStatus = { healthy: boolean; version?: string }
|
|
16
|
+
|
|
17
|
+
async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise<ServerStatus> {
|
|
18
|
+
const sdk = createOpencodeClient({
|
|
19
|
+
baseUrl: url,
|
|
20
|
+
fetch,
|
|
21
|
+
signal: AbortSignal.timeout(3000),
|
|
22
|
+
})
|
|
23
|
+
return sdk.global
|
|
24
|
+
.health()
|
|
25
|
+
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
|
|
26
|
+
.catch(() => ({ healthy: false }))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DialogSelectServer() {
|
|
30
|
+
const navigate = useNavigate()
|
|
31
|
+
const dialog = useDialog()
|
|
32
|
+
const server = useServer()
|
|
33
|
+
const platform = usePlatform()
|
|
34
|
+
const language = useLanguage()
|
|
35
|
+
const [store, setStore] = createStore({
|
|
36
|
+
url: "",
|
|
37
|
+
adding: false,
|
|
38
|
+
error: "",
|
|
39
|
+
status: {} as Record<string, ServerStatus | undefined>,
|
|
40
|
+
})
|
|
41
|
+
const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
|
|
42
|
+
const isDesktop = platform.platform === "desktop"
|
|
43
|
+
|
|
44
|
+
const items = createMemo(() => {
|
|
45
|
+
const current = server.url
|
|
46
|
+
const list = server.list
|
|
47
|
+
if (!current) return list
|
|
48
|
+
if (!list.includes(current)) return [current, ...list]
|
|
49
|
+
return [current, ...list.filter((x) => x !== current)]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
|
|
53
|
+
|
|
54
|
+
const sortedItems = createMemo(() => {
|
|
55
|
+
const list = items()
|
|
56
|
+
if (!list.length) return list
|
|
57
|
+
const active = current()
|
|
58
|
+
const order = new Map(list.map((url, index) => [url, index] as const))
|
|
59
|
+
const rank = (value?: ServerStatus) => {
|
|
60
|
+
if (value?.healthy === true) return 0
|
|
61
|
+
if (value?.healthy === false) return 2
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
return list.slice().sort((a, b) => {
|
|
65
|
+
if (a === active) return -1
|
|
66
|
+
if (b === active) return 1
|
|
67
|
+
const diff = rank(store.status[a]) - rank(store.status[b])
|
|
68
|
+
if (diff !== 0) return diff
|
|
69
|
+
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
async function refreshHealth() {
|
|
74
|
+
const results: Record<string, ServerStatus> = {}
|
|
75
|
+
await Promise.all(
|
|
76
|
+
items().map(async (url) => {
|
|
77
|
+
results[url] = await checkHealth(url, platform.fetch)
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
setStore("status", reconcile(results))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
createEffect(() => {
|
|
84
|
+
items()
|
|
85
|
+
refreshHealth()
|
|
86
|
+
const interval = setInterval(refreshHealth, 10_000)
|
|
87
|
+
onCleanup(() => clearInterval(interval))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function select(value: string, persist?: boolean) {
|
|
91
|
+
if (!persist && store.status[value]?.healthy === false) return
|
|
92
|
+
dialog.close()
|
|
93
|
+
if (persist) {
|
|
94
|
+
server.add(value)
|
|
95
|
+
navigate("/")
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
server.setActive(value)
|
|
99
|
+
navigate("/")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
const value = normalizeServerUrl(store.url)
|
|
105
|
+
if (!value) return
|
|
106
|
+
|
|
107
|
+
setStore("adding", true)
|
|
108
|
+
setStore("error", "")
|
|
109
|
+
|
|
110
|
+
const result = await checkHealth(value, platform.fetch)
|
|
111
|
+
setStore("adding", false)
|
|
112
|
+
|
|
113
|
+
if (!result.healthy) {
|
|
114
|
+
setStore("error", language.t("dialog.server.add.error"))
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setStore("url", "")
|
|
119
|
+
select(value, true)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function handleRemove(url: string) {
|
|
123
|
+
server.remove(url)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Dialog title={language.t("dialog.server.title")} description={language.t("dialog.server.description")}>
|
|
128
|
+
<div class="flex flex-col gap-4 pb-4">
|
|
129
|
+
<List
|
|
130
|
+
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
|
|
131
|
+
emptyMessage={language.t("dialog.server.empty")}
|
|
132
|
+
items={sortedItems}
|
|
133
|
+
key={(x) => x}
|
|
134
|
+
current={current()}
|
|
135
|
+
onSelect={(x) => {
|
|
136
|
+
if (x) select(x)
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{(i) => (
|
|
140
|
+
<div class="flex items-center gap-2 min-w-0 flex-1 group/item">
|
|
141
|
+
<div
|
|
142
|
+
class="flex items-center gap-2 min-w-0 flex-1"
|
|
143
|
+
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
classList={{
|
|
147
|
+
"size-1.5 rounded-full shrink-0": true,
|
|
148
|
+
"bg-icon-success-base": store.status[i]?.healthy === true,
|
|
149
|
+
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
|
150
|
+
"bg-border-weak-base": store.status[i] === undefined,
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
<span class="truncate">{serverDisplayName(i)}</span>
|
|
154
|
+
<span class="text-text-weak">{store.status[i]?.version}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<Show when={current() !== i && server.list.includes(i)}>
|
|
157
|
+
<IconButton
|
|
158
|
+
icon="circle-x"
|
|
159
|
+
variant="ghost"
|
|
160
|
+
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
|
|
161
|
+
aria-label={language.t("dialog.server.action.remove")}
|
|
162
|
+
onClick={(e) => {
|
|
163
|
+
e.stopPropagation()
|
|
164
|
+
handleRemove(i)
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</Show>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</List>
|
|
171
|
+
|
|
172
|
+
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
|
173
|
+
<div class="px-3">
|
|
174
|
+
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.add.title")}</h3>
|
|
175
|
+
</div>
|
|
176
|
+
<form onSubmit={handleSubmit}>
|
|
177
|
+
<div class="flex items-start gap-2">
|
|
178
|
+
<div class="flex-1 min-w-0 h-auto">
|
|
179
|
+
<TextField
|
|
180
|
+
type="text"
|
|
181
|
+
label={language.t("dialog.server.add.url")}
|
|
182
|
+
hideLabel
|
|
183
|
+
placeholder={language.t("dialog.server.add.placeholder")}
|
|
184
|
+
value={store.url}
|
|
185
|
+
onChange={(v) => {
|
|
186
|
+
setStore("url", v)
|
|
187
|
+
setStore("error", "")
|
|
188
|
+
}}
|
|
189
|
+
validationState={store.error ? "invalid" : "valid"}
|
|
190
|
+
error={store.error}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
|
|
194
|
+
{store.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</form>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<Show when={isDesktop}>
|
|
201
|
+
<div class="mt-6 px-3 flex flex-col gap-1.5">
|
|
202
|
+
<div class="px-3">
|
|
203
|
+
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3>
|
|
204
|
+
<p class="text-12-regular text-text-weak mt-1">{language.t("dialog.server.default.description")}</p>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="flex items-center gap-2 px-3 py-2">
|
|
207
|
+
<Show
|
|
208
|
+
when={defaultUrl()}
|
|
209
|
+
fallback={
|
|
210
|
+
<Show
|
|
211
|
+
when={server.url}
|
|
212
|
+
fallback={
|
|
213
|
+
<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
<Button
|
|
217
|
+
variant="secondary"
|
|
218
|
+
size="small"
|
|
219
|
+
onClick={async () => {
|
|
220
|
+
await platform.setDefaultServerUrl?.(server.url)
|
|
221
|
+
defaultUrlActions.refetch(server.url)
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
{language.t("dialog.server.default.set")}
|
|
225
|
+
</Button>
|
|
226
|
+
</Show>
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
<div class="flex items-center gap-2 flex-1 min-w-0">
|
|
230
|
+
<span class="truncate text-14-regular">{serverDisplayName(defaultUrl()!)}</span>
|
|
231
|
+
</div>
|
|
232
|
+
<Button
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="small"
|
|
235
|
+
onClick={async () => {
|
|
236
|
+
await platform.setDefaultServerUrl?.(null)
|
|
237
|
+
defaultUrlActions.refetch()
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
{language.t("dialog.server.default.clear")}
|
|
241
|
+
</Button>
|
|
242
|
+
</Show>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</Show>
|
|
246
|
+
</div>
|
|
247
|
+
</Dialog>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Component } from "solid-js"
|
|
2
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
3
|
+
import { Tabs } from "@jonsoc/ui/tabs"
|
|
4
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
5
|
+
import { useLanguage } from "@/context/language"
|
|
6
|
+
import { usePlatform } from "@/context/platform"
|
|
7
|
+
import { SettingsGeneral } from "./settings-general"
|
|
8
|
+
import { SettingsKeybinds } from "./settings-keybinds"
|
|
9
|
+
import { SettingsPermissions } from "./settings-permissions"
|
|
10
|
+
import { SettingsProviders } from "./settings-providers"
|
|
11
|
+
import { SettingsModels } from "./settings-models"
|
|
12
|
+
import { SettingsAgents } from "./settings-agents"
|
|
13
|
+
import { SettingsCommands } from "./settings-commands"
|
|
14
|
+
import { SettingsMcp } from "./settings-mcp"
|
|
15
|
+
|
|
16
|
+
export const DialogSettings: Component = () => {
|
|
17
|
+
const language = useLanguage()
|
|
18
|
+
const platform = usePlatform()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Dialog size="x-large">
|
|
22
|
+
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
|
|
23
|
+
<Tabs.List>
|
|
24
|
+
<div
|
|
25
|
+
style={{
|
|
26
|
+
display: "flex",
|
|
27
|
+
"flex-direction": "column",
|
|
28
|
+
"justify-content": "space-between",
|
|
29
|
+
height: "100%",
|
|
30
|
+
width: "100%",
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
display: "flex",
|
|
36
|
+
"flex-direction": "column",
|
|
37
|
+
gap: "12px",
|
|
38
|
+
width: "100%",
|
|
39
|
+
"padding-top": "12px",
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
|
43
|
+
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
|
|
44
|
+
<Tabs.Trigger value="general">
|
|
45
|
+
<Icon name="sliders" />
|
|
46
|
+
{language.t("settings.tab.general")}
|
|
47
|
+
</Tabs.Trigger>
|
|
48
|
+
<Tabs.Trigger value="shortcuts">
|
|
49
|
+
<Icon name="keyboard" />
|
|
50
|
+
{language.t("settings.tab.shortcuts")}
|
|
51
|
+
</Tabs.Trigger>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
|
55
|
+
<span>JonsOC Desktop</span>
|
|
56
|
+
<span class="text-11-regular">v{platform.version}</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
{/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
|
|
60
|
+
{/* <Tabs.Trigger value="permissions"> */}
|
|
61
|
+
{/* <Icon name="checklist" /> */}
|
|
62
|
+
{/* Permissions */}
|
|
63
|
+
{/* </Tabs.Trigger> */}
|
|
64
|
+
{/* <Tabs.Trigger value="providers"> */}
|
|
65
|
+
{/* <Icon name="server" /> */}
|
|
66
|
+
{/* Providers */}
|
|
67
|
+
{/* </Tabs.Trigger> */}
|
|
68
|
+
{/* <Tabs.Trigger value="models"> */}
|
|
69
|
+
{/* <Icon name="brain" /> */}
|
|
70
|
+
{/* Models */}
|
|
71
|
+
{/* </Tabs.Trigger> */}
|
|
72
|
+
{/* <Tabs.Trigger value="agents"> */}
|
|
73
|
+
{/* <Icon name="task" /> */}
|
|
74
|
+
{/* Agents */}
|
|
75
|
+
{/* </Tabs.Trigger> */}
|
|
76
|
+
{/* <Tabs.Trigger value="commands"> */}
|
|
77
|
+
{/* <Icon name="console" /> */}
|
|
78
|
+
{/* Commands */}
|
|
79
|
+
{/* </Tabs.Trigger> */}
|
|
80
|
+
{/* <Tabs.Trigger value="mcp"> */}
|
|
81
|
+
{/* <Icon name="mcp" /> */}
|
|
82
|
+
{/* MCP */}
|
|
83
|
+
{/* </Tabs.Trigger> */}
|
|
84
|
+
</Tabs.List>
|
|
85
|
+
<Tabs.Content value="general" class="no-scrollbar">
|
|
86
|
+
<SettingsGeneral />
|
|
87
|
+
</Tabs.Content>
|
|
88
|
+
<Tabs.Content value="shortcuts" class="no-scrollbar">
|
|
89
|
+
<SettingsKeybinds />
|
|
90
|
+
</Tabs.Content>
|
|
91
|
+
{/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
|
|
92
|
+
{/* <SettingsPermissions /> */}
|
|
93
|
+
{/* </Tabs.Content> */}
|
|
94
|
+
{/* <Tabs.Content value="providers" class="no-scrollbar"> */}
|
|
95
|
+
{/* <SettingsProviders /> */}
|
|
96
|
+
{/* </Tabs.Content> */}
|
|
97
|
+
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
|
|
98
|
+
{/* <SettingsModels /> */}
|
|
99
|
+
{/* </Tabs.Content> */}
|
|
100
|
+
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
|
101
|
+
{/* <SettingsAgents /> */}
|
|
102
|
+
{/* </Tabs.Content> */}
|
|
103
|
+
{/* <Tabs.Content value="commands" class="no-scrollbar"> */}
|
|
104
|
+
{/* <SettingsCommands /> */}
|
|
105
|
+
{/* </Tabs.Content> */}
|
|
106
|
+
{/* <Tabs.Content value="mcp" class="no-scrollbar"> */}
|
|
107
|
+
{/* <SettingsMcp /> */}
|
|
108
|
+
{/* </Tabs.Content> */}
|
|
109
|
+
</Tabs>
|
|
110
|
+
</Dialog>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useLocal, type LocalFile } from "@/context/local"
|
|
2
|
+
import { Collapsible } from "@jonsoc/ui/collapsible"
|
|
3
|
+
import { FileIcon } from "@jonsoc/ui/file-icon"
|
|
4
|
+
import { Tooltip } from "@jonsoc/ui/tooltip"
|
|
5
|
+
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
|
|
6
|
+
import { Dynamic } from "solid-js/web"
|
|
7
|
+
|
|
8
|
+
export default function FileTree(props: {
|
|
9
|
+
path: string
|
|
10
|
+
class?: string
|
|
11
|
+
nodeClass?: string
|
|
12
|
+
level?: number
|
|
13
|
+
onFileClick?: (file: LocalFile) => void
|
|
14
|
+
}) {
|
|
15
|
+
const local = useLocal()
|
|
16
|
+
const level = props.level ?? 0
|
|
17
|
+
|
|
18
|
+
const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
|
|
19
|
+
<Dynamic
|
|
20
|
+
component={p.as ?? "div"}
|
|
21
|
+
classList={{
|
|
22
|
+
"p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
|
|
23
|
+
// "bg-background-element": local.file.active()?.path === p.node.path,
|
|
24
|
+
[props.nodeClass ?? ""]: !!props.nodeClass,
|
|
25
|
+
}}
|
|
26
|
+
style={`padding-left: ${level * 10}px`}
|
|
27
|
+
draggable={true}
|
|
28
|
+
onDragStart={(e: any) => {
|
|
29
|
+
const evt = e as globalThis.DragEvent
|
|
30
|
+
evt.dataTransfer!.effectAllowed = "copy"
|
|
31
|
+
evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
|
|
32
|
+
|
|
33
|
+
// Create custom drag image without margins
|
|
34
|
+
const dragImage = document.createElement("div")
|
|
35
|
+
dragImage.className =
|
|
36
|
+
"flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
|
|
37
|
+
dragImage.style.position = "absolute"
|
|
38
|
+
dragImage.style.top = "-1000px"
|
|
39
|
+
|
|
40
|
+
// Copy only the icon and text content without padding
|
|
41
|
+
const icon = e.currentTarget.querySelector("svg")
|
|
42
|
+
const text = e.currentTarget.querySelector("span")
|
|
43
|
+
if (icon && text) {
|
|
44
|
+
dragImage.innerHTML = icon.outerHTML + text.outerHTML
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
document.body.appendChild(dragImage)
|
|
48
|
+
evt.dataTransfer!.setDragImage(dragImage, 0, 12)
|
|
49
|
+
setTimeout(() => document.body.removeChild(dragImage), 0)
|
|
50
|
+
}}
|
|
51
|
+
{...p}
|
|
52
|
+
>
|
|
53
|
+
{p.children}
|
|
54
|
+
<span
|
|
55
|
+
classList={{
|
|
56
|
+
"text-xs whitespace-nowrap truncate": true,
|
|
57
|
+
"text-text-muted/40": p.node.ignored,
|
|
58
|
+
"text-text-muted/80": !p.node.ignored,
|
|
59
|
+
// "!text-text": local.file.active()?.path === p.node.path,
|
|
60
|
+
// "!text-primary": local.file.changed(p.node.path),
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{p.node.name}
|
|
64
|
+
</span>
|
|
65
|
+
{/* <Show when={local.file.changed(p.node.path)}> */}
|
|
66
|
+
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
|
|
67
|
+
{/* </Show> */}
|
|
68
|
+
</Dynamic>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div class={`flex flex-col ${props.class}`}>
|
|
73
|
+
<For each={local.file.children(props.path)}>
|
|
74
|
+
{(node) => (
|
|
75
|
+
<Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
|
|
76
|
+
<Switch>
|
|
77
|
+
<Match when={node.type === "directory"}>
|
|
78
|
+
<Collapsible
|
|
79
|
+
variant="ghost"
|
|
80
|
+
class="w-full"
|
|
81
|
+
forceMount={false}
|
|
82
|
+
// open={local.file.node(node.path)?.expanded}
|
|
83
|
+
onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
|
|
84
|
+
>
|
|
85
|
+
<Collapsible.Trigger>
|
|
86
|
+
<Node node={node}>
|
|
87
|
+
<Collapsible.Arrow class="text-text-muted/60 ml-1" />
|
|
88
|
+
<FileIcon
|
|
89
|
+
node={node}
|
|
90
|
+
// expanded={local.file.node(node.path).expanded}
|
|
91
|
+
class="text-text-muted/60 -ml-1"
|
|
92
|
+
/>
|
|
93
|
+
</Node>
|
|
94
|
+
</Collapsible.Trigger>
|
|
95
|
+
<Collapsible.Content>
|
|
96
|
+
<FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
|
|
97
|
+
</Collapsible.Content>
|
|
98
|
+
</Collapsible>
|
|
99
|
+
</Match>
|
|
100
|
+
<Match when={node.type === "file"}>
|
|
101
|
+
<Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
|
|
102
|
+
<div class="w-4 shrink-0" />
|
|
103
|
+
<FileIcon node={node} class="text-primary" />
|
|
104
|
+
</Node>
|
|
105
|
+
</Match>
|
|
106
|
+
</Switch>
|
|
107
|
+
</Tooltip>
|
|
108
|
+
)}
|
|
109
|
+
</For>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ComponentProps, splitProps } from "solid-js"
|
|
2
|
+
import { usePlatform } from "@/context/platform"
|
|
3
|
+
|
|
4
|
+
export interface LinkProps extends ComponentProps<"button"> {
|
|
5
|
+
href: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Link(props: LinkProps) {
|
|
9
|
+
const platform = usePlatform()
|
|
10
|
+
const [local, rest] = splitProps(props, ["href", "children"])
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
|
|
14
|
+
{local.children}
|
|
15
|
+
</button>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Show, type Component } from "solid-js"
|
|
2
|
+
import { useLanguage } from "@/context/language"
|
|
3
|
+
|
|
4
|
+
type InputKey = "text" | "image" | "audio" | "video" | "pdf"
|
|
5
|
+
type InputMap = Record<InputKey, boolean>
|
|
6
|
+
|
|
7
|
+
type ModelInfo = {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
provider: {
|
|
11
|
+
name: string
|
|
12
|
+
}
|
|
13
|
+
capabilities?: {
|
|
14
|
+
reasoning: boolean
|
|
15
|
+
input: InputMap
|
|
16
|
+
}
|
|
17
|
+
modalities?: {
|
|
18
|
+
input: Array<string>
|
|
19
|
+
}
|
|
20
|
+
reasoning?: boolean
|
|
21
|
+
limit: {
|
|
22
|
+
context: number
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
|
|
27
|
+
const language = useLanguage()
|
|
28
|
+
const sourceName = (model: ModelInfo) => {
|
|
29
|
+
const value = `${model.id} ${model.name}`.toLowerCase()
|
|
30
|
+
|
|
31
|
+
if (/claude|anthropic/.test(value)) return language.t("model.provider.anthropic")
|
|
32
|
+
if (/gpt|o[1-4]|codex|openai/.test(value)) return language.t("model.provider.openai")
|
|
33
|
+
if (/gemini|palm|bard|google/.test(value)) return language.t("model.provider.google")
|
|
34
|
+
if (/grok|xai/.test(value)) return language.t("model.provider.xai")
|
|
35
|
+
if (/llama|meta/.test(value)) return language.t("model.provider.meta")
|
|
36
|
+
|
|
37
|
+
return model.provider.name
|
|
38
|
+
}
|
|
39
|
+
const inputLabel = (value: string) => {
|
|
40
|
+
if (value === "text") return language.t("model.input.text")
|
|
41
|
+
if (value === "image") return language.t("model.input.image")
|
|
42
|
+
if (value === "audio") return language.t("model.input.audio")
|
|
43
|
+
if (value === "video") return language.t("model.input.video")
|
|
44
|
+
if (value === "pdf") return language.t("model.input.pdf")
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
const title = () => {
|
|
48
|
+
const tags: Array<string> = []
|
|
49
|
+
if (props.latest) tags.push(language.t("model.tag.latest"))
|
|
50
|
+
if (props.free) tags.push(language.t("model.tag.free"))
|
|
51
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
|
52
|
+
return `${sourceName(props.model)} ${props.model.name}${suffix}`
|
|
53
|
+
}
|
|
54
|
+
const inputs = () => {
|
|
55
|
+
if (props.model.capabilities) {
|
|
56
|
+
const input = props.model.capabilities.input
|
|
57
|
+
const order: Array<InputKey> = ["text", "image", "audio", "video", "pdf"]
|
|
58
|
+
const entries = order.filter((key) => input[key]).map((key) => inputLabel(key))
|
|
59
|
+
return entries.length ? entries.join(", ") : undefined
|
|
60
|
+
}
|
|
61
|
+
const raw = props.model.modalities?.input
|
|
62
|
+
if (!raw) return
|
|
63
|
+
const entries = raw.map((value) => inputLabel(value))
|
|
64
|
+
return entries.length ? entries.join(", ") : undefined
|
|
65
|
+
}
|
|
66
|
+
const reasoning = () => {
|
|
67
|
+
if (props.model.capabilities)
|
|
68
|
+
return props.model.capabilities.reasoning
|
|
69
|
+
? language.t("model.tooltip.reasoning.allowed")
|
|
70
|
+
: language.t("model.tooltip.reasoning.none")
|
|
71
|
+
return props.model.reasoning
|
|
72
|
+
? language.t("model.tooltip.reasoning.allowed")
|
|
73
|
+
: language.t("model.tooltip.reasoning.none")
|
|
74
|
+
}
|
|
75
|
+
const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div class="flex flex-col gap-1 py-1">
|
|
79
|
+
<div class="text-13-medium">{title()}</div>
|
|
80
|
+
<Show when={inputs()}>
|
|
81
|
+
{(value) => (
|
|
82
|
+
<div class="text-12-regular text-text-invert-base">
|
|
83
|
+
{language.t("model.tooltip.allows", { inputs: value() })}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</Show>
|
|
87
|
+
<div class="text-12-regular text-text-invert-base">{reasoning()}</div>
|
|
88
|
+
<div class="text-12-regular text-text-invert-base">{context()}</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|