@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,196 @@
|
|
|
1
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
2
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
3
|
+
import { FileIcon } from "@jonsoc/ui/file-icon"
|
|
4
|
+
import { Keybind } from "@jonsoc/ui/keybind"
|
|
5
|
+
import { List } from "@jonsoc/ui/list"
|
|
6
|
+
import { getDirectory, getFilename } from "@jonsoc/util/path"
|
|
7
|
+
import { useParams } from "@solidjs/router"
|
|
8
|
+
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
|
|
9
|
+
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
|
|
10
|
+
import { useLayout } from "@/context/layout"
|
|
11
|
+
import { useFile } from "@/context/file"
|
|
12
|
+
import { useLanguage } from "@/context/language"
|
|
13
|
+
|
|
14
|
+
type EntryType = "command" | "file"
|
|
15
|
+
|
|
16
|
+
type Entry = {
|
|
17
|
+
id: string
|
|
18
|
+
type: EntryType
|
|
19
|
+
title: string
|
|
20
|
+
description?: string
|
|
21
|
+
keybind?: string
|
|
22
|
+
category: string
|
|
23
|
+
option?: CommandOption
|
|
24
|
+
path?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DialogSelectFile() {
|
|
28
|
+
const command = useCommand()
|
|
29
|
+
const language = useLanguage()
|
|
30
|
+
const layout = useLayout()
|
|
31
|
+
const file = useFile()
|
|
32
|
+
const dialog = useDialog()
|
|
33
|
+
const params = useParams()
|
|
34
|
+
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
35
|
+
const tabs = createMemo(() => layout.tabs(sessionKey))
|
|
36
|
+
const view = createMemo(() => layout.view(sessionKey))
|
|
37
|
+
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
|
38
|
+
const [grouped, setGrouped] = createSignal(false)
|
|
39
|
+
const common = [
|
|
40
|
+
"session.new",
|
|
41
|
+
"workspace.new",
|
|
42
|
+
"session.previous",
|
|
43
|
+
"session.next",
|
|
44
|
+
"terminal.toggle",
|
|
45
|
+
"review.toggle",
|
|
46
|
+
]
|
|
47
|
+
const limit = 5
|
|
48
|
+
|
|
49
|
+
const allowed = createMemo(() =>
|
|
50
|
+
command.options.filter(
|
|
51
|
+
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const commandItem = (option: CommandOption): Entry => ({
|
|
56
|
+
id: "command:" + option.id,
|
|
57
|
+
type: "command",
|
|
58
|
+
title: option.title,
|
|
59
|
+
description: option.description,
|
|
60
|
+
keybind: option.keybind,
|
|
61
|
+
category: language.t("palette.group.commands"),
|
|
62
|
+
option,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const fileItem = (path: string): Entry => ({
|
|
66
|
+
id: "file:" + path,
|
|
67
|
+
type: "file",
|
|
68
|
+
title: path,
|
|
69
|
+
category: language.t("palette.group.files"),
|
|
70
|
+
path,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const list = createMemo(() => allowed().map(commandItem))
|
|
74
|
+
|
|
75
|
+
const picks = createMemo(() => {
|
|
76
|
+
const all = allowed()
|
|
77
|
+
const order = new Map(common.map((id, index) => [id, index]))
|
|
78
|
+
const picked = all.filter((option) => order.has(option.id))
|
|
79
|
+
const base = picked.length ? picked : all.slice(0, limit)
|
|
80
|
+
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
|
81
|
+
return sorted.map(commandItem)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const recent = createMemo(() => {
|
|
85
|
+
const all = tabs().all()
|
|
86
|
+
const active = tabs().active()
|
|
87
|
+
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
|
88
|
+
const seen = new Set<string>()
|
|
89
|
+
const items: Entry[] = []
|
|
90
|
+
|
|
91
|
+
for (const item of order) {
|
|
92
|
+
const path = file.pathFromTab(item)
|
|
93
|
+
if (!path) continue
|
|
94
|
+
if (seen.has(path)) continue
|
|
95
|
+
seen.add(path)
|
|
96
|
+
items.push(fileItem(path))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return items.slice(0, limit)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const items = async (filter: string) => {
|
|
103
|
+
const query = filter.trim()
|
|
104
|
+
setGrouped(query.length > 0)
|
|
105
|
+
if (!query) return [...picks(), ...recent()]
|
|
106
|
+
const files = await file.searchFiles(query)
|
|
107
|
+
const entries = files.map(fileItem)
|
|
108
|
+
return [...list(), ...entries]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const handleMove = (item: Entry | undefined) => {
|
|
112
|
+
state.cleanup?.()
|
|
113
|
+
if (!item) return
|
|
114
|
+
if (item.type !== "command") return
|
|
115
|
+
state.cleanup = item.option?.onHighlight?.()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const open = (path: string) => {
|
|
119
|
+
const value = file.tab(path)
|
|
120
|
+
tabs().open(value)
|
|
121
|
+
file.load(path)
|
|
122
|
+
view().reviewPanel.open()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const handleSelect = (item: Entry | undefined) => {
|
|
126
|
+
if (!item) return
|
|
127
|
+
state.committed = true
|
|
128
|
+
state.cleanup = undefined
|
|
129
|
+
dialog.close()
|
|
130
|
+
|
|
131
|
+
if (item.type === "command") {
|
|
132
|
+
item.option?.onSelect?.("palette")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!item.path) return
|
|
137
|
+
open(item.path)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onCleanup(() => {
|
|
141
|
+
if (state.committed) return
|
|
142
|
+
state.cleanup?.()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Dialog class="pt-3 pb-0 !max-h-[480px]">
|
|
147
|
+
<List
|
|
148
|
+
search={{
|
|
149
|
+
placeholder: language.t("palette.search.placeholder"),
|
|
150
|
+
autofocus: true,
|
|
151
|
+
hideIcon: true,
|
|
152
|
+
class: "pl-3 pr-2 !mb-0",
|
|
153
|
+
}}
|
|
154
|
+
emptyMessage={language.t("palette.empty")}
|
|
155
|
+
loadingMessage={language.t("common.loading")}
|
|
156
|
+
items={items}
|
|
157
|
+
key={(item) => item.id}
|
|
158
|
+
filterKeys={["title", "description", "category"]}
|
|
159
|
+
groupBy={(item) => item.category}
|
|
160
|
+
onMove={handleMove}
|
|
161
|
+
onSelect={handleSelect}
|
|
162
|
+
>
|
|
163
|
+
{(item) => (
|
|
164
|
+
<Show
|
|
165
|
+
when={item.type === "command"}
|
|
166
|
+
fallback={
|
|
167
|
+
<div class="w-full flex items-center justify-between rounded-md pl-1">
|
|
168
|
+
<div class="flex items-center gap-x-3 grow min-w-0">
|
|
169
|
+
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
|
|
170
|
+
<div class="flex items-center text-14-regular">
|
|
171
|
+
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
|
172
|
+
{getDirectory(item.path ?? "")}
|
|
173
|
+
</span>
|
|
174
|
+
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
}
|
|
179
|
+
>
|
|
180
|
+
<div class="w-full flex items-center justify-between gap-4 pl-1">
|
|
181
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
182
|
+
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
|
183
|
+
<Show when={item.description}>
|
|
184
|
+
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
|
185
|
+
</Show>
|
|
186
|
+
</div>
|
|
187
|
+
<Show when={item.keybind}>
|
|
188
|
+
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
|
189
|
+
</Show>
|
|
190
|
+
</div>
|
|
191
|
+
</Show>
|
|
192
|
+
)}
|
|
193
|
+
</List>
|
|
194
|
+
</Dialog>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Component, createMemo, createSignal, Show } from "solid-js"
|
|
2
|
+
import { useSync } from "@/context/sync"
|
|
3
|
+
import { useSDK } from "@/context/sdk"
|
|
4
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
5
|
+
import { List } from "@jonsoc/ui/list"
|
|
6
|
+
import { Switch } from "@jonsoc/ui/switch"
|
|
7
|
+
import { useLanguage } from "@/context/language"
|
|
8
|
+
|
|
9
|
+
export const DialogSelectMcp: Component = () => {
|
|
10
|
+
const sync = useSync()
|
|
11
|
+
const sdk = useSDK()
|
|
12
|
+
const language = useLanguage()
|
|
13
|
+
const [loading, setLoading] = createSignal<string | null>(null)
|
|
14
|
+
|
|
15
|
+
const items = createMemo(() =>
|
|
16
|
+
Object.entries(sync.data.mcp ?? {})
|
|
17
|
+
.map(([name, status]) => ({ name, status: status.status }))
|
|
18
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const toggle = async (name: string) => {
|
|
22
|
+
if (loading()) return
|
|
23
|
+
setLoading(name)
|
|
24
|
+
const status = sync.data.mcp[name]
|
|
25
|
+
if (status?.status === "connected") {
|
|
26
|
+
await sdk.client.mcp.disconnect({ name })
|
|
27
|
+
} else {
|
|
28
|
+
await sdk.client.mcp.connect({ name })
|
|
29
|
+
}
|
|
30
|
+
const result = await sdk.client.mcp.status()
|
|
31
|
+
if (result.data) sync.set("mcp", result.data)
|
|
32
|
+
setLoading(null)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
|
36
|
+
const totalCount = createMemo(() => items().length)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Dialog
|
|
40
|
+
title={language.t("dialog.mcp.title")}
|
|
41
|
+
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
|
|
42
|
+
>
|
|
43
|
+
<List
|
|
44
|
+
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
|
45
|
+
emptyMessage={language.t("dialog.mcp.empty")}
|
|
46
|
+
key={(x) => x?.name ?? ""}
|
|
47
|
+
items={items}
|
|
48
|
+
filterKeys={["name", "status"]}
|
|
49
|
+
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
|
50
|
+
onSelect={(x) => {
|
|
51
|
+
if (x) toggle(x.name)
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{(i) => {
|
|
55
|
+
const mcpStatus = () => sync.data.mcp[i.name]
|
|
56
|
+
const status = () => mcpStatus()?.status
|
|
57
|
+
const error = () => {
|
|
58
|
+
const s = mcpStatus()
|
|
59
|
+
return s?.status === "failed" ? s.error : undefined
|
|
60
|
+
}
|
|
61
|
+
const enabled = () => status() === "connected"
|
|
62
|
+
return (
|
|
63
|
+
<div class="w-full flex items-center justify-between gap-x-3">
|
|
64
|
+
<div class="flex flex-col gap-0.5 min-w-0">
|
|
65
|
+
<div class="flex items-center gap-2">
|
|
66
|
+
<span class="truncate">{i.name}</span>
|
|
67
|
+
<Show when={status() === "connected"}>
|
|
68
|
+
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.connected")}</span>
|
|
69
|
+
</Show>
|
|
70
|
+
<Show when={status() === "failed"}>
|
|
71
|
+
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.failed")}</span>
|
|
72
|
+
</Show>
|
|
73
|
+
<Show when={status() === "needs_auth"}>
|
|
74
|
+
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.needs_auth")}</span>
|
|
75
|
+
</Show>
|
|
76
|
+
<Show when={status() === "disabled"}>
|
|
77
|
+
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span>
|
|
78
|
+
</Show>
|
|
79
|
+
<Show when={loading() === i.name}>
|
|
80
|
+
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
|
81
|
+
</Show>
|
|
82
|
+
</div>
|
|
83
|
+
<Show when={error()}>
|
|
84
|
+
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
|
85
|
+
</Show>
|
|
86
|
+
</div>
|
|
87
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
88
|
+
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}}
|
|
93
|
+
</List>
|
|
94
|
+
</Dialog>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Button } from "@jonsoc/ui/button"
|
|
2
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
3
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
4
|
+
import type { IconName } from "@jonsoc/ui/icons/provider"
|
|
5
|
+
import { List, type ListRef } from "@jonsoc/ui/list"
|
|
6
|
+
import { ProviderIcon } from "@jonsoc/ui/provider-icon"
|
|
7
|
+
import { Tag } from "@jonsoc/ui/tag"
|
|
8
|
+
import { Tooltip } from "@jonsoc/ui/tooltip"
|
|
9
|
+
import { type Component, onCleanup, onMount, Show } from "solid-js"
|
|
10
|
+
import { useLocal } from "@/context/local"
|
|
11
|
+
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
12
|
+
import { DialogConnectProvider } from "./dialog-connect-provider"
|
|
13
|
+
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
14
|
+
import { ModelTooltip } from "./model-tooltip"
|
|
15
|
+
import { useLanguage } from "@/context/language"
|
|
16
|
+
|
|
17
|
+
export const DialogSelectModelUnpaid: Component = () => {
|
|
18
|
+
const local = useLocal()
|
|
19
|
+
const dialog = useDialog()
|
|
20
|
+
const providers = useProviders()
|
|
21
|
+
const language = useLanguage()
|
|
22
|
+
|
|
23
|
+
let listRef: ListRef | undefined
|
|
24
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
25
|
+
if (e.key === "Escape") return
|
|
26
|
+
listRef?.onKeyDown(e)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
document.addEventListener("keydown", handleKey)
|
|
31
|
+
onCleanup(() => {
|
|
32
|
+
document.removeEventListener("keydown", handleKey)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Dialog title={language.t("dialog.model.select.title")}>
|
|
38
|
+
<div class="flex flex-col gap-3 px-2.5">
|
|
39
|
+
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
|
|
40
|
+
<List
|
|
41
|
+
ref={(ref) => (listRef = ref)}
|
|
42
|
+
items={local.model.list}
|
|
43
|
+
current={local.model.current()}
|
|
44
|
+
key={(x) => `${x.provider.id}:${x.id}`}
|
|
45
|
+
itemWrapper={(item, node) => (
|
|
46
|
+
<Tooltip
|
|
47
|
+
class="w-full"
|
|
48
|
+
placement="right-start"
|
|
49
|
+
gutter={12}
|
|
50
|
+
value={
|
|
51
|
+
<ModelTooltip
|
|
52
|
+
model={item}
|
|
53
|
+
latest={item.latest}
|
|
54
|
+
free={item.provider.id === "jonsoc" && (!item.cost || item.cost.input === 0)}
|
|
55
|
+
/>
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
{node}
|
|
59
|
+
</Tooltip>
|
|
60
|
+
)}
|
|
61
|
+
onSelect={(x) => {
|
|
62
|
+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
|
63
|
+
recent: true,
|
|
64
|
+
})
|
|
65
|
+
dialog.close()
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{(i) => (
|
|
69
|
+
<div class="w-full flex items-center gap-x-2.5">
|
|
70
|
+
<span>{i.name}</span>
|
|
71
|
+
<Tag>{language.t("model.tag.free")}</Tag>
|
|
72
|
+
<Show when={i.latest}>
|
|
73
|
+
<Tag>{language.t("model.tag.latest")}</Tag>
|
|
74
|
+
</Show>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</List>
|
|
78
|
+
<div />
|
|
79
|
+
<div />
|
|
80
|
+
</div>
|
|
81
|
+
<div class="px-1.5 pb-1.5">
|
|
82
|
+
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
|
83
|
+
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
|
84
|
+
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
|
|
85
|
+
<div class="w-full">
|
|
86
|
+
<List
|
|
87
|
+
class="w-full px-0"
|
|
88
|
+
key={(x) => x?.id}
|
|
89
|
+
items={providers.popular}
|
|
90
|
+
activeIcon="plus-small"
|
|
91
|
+
sortBy={(a, b) => {
|
|
92
|
+
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
|
93
|
+
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
|
94
|
+
return a.name.localeCompare(b.name)
|
|
95
|
+
}}
|
|
96
|
+
onSelect={(x) => {
|
|
97
|
+
if (!x) return
|
|
98
|
+
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{(i) => (
|
|
102
|
+
<div class="w-full flex items-center gap-x-3">
|
|
103
|
+
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
|
104
|
+
<span>{i.name}</span>
|
|
105
|
+
<Show when={i.id === "jonsoc"}>
|
|
106
|
+
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
|
107
|
+
</Show>
|
|
108
|
+
<Show when={i.id === "anthropic"}>
|
|
109
|
+
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
|
110
|
+
</Show>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</List>
|
|
114
|
+
<Button
|
|
115
|
+
variant="ghost"
|
|
116
|
+
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
|
117
|
+
icon="dot-grid"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
dialog.show(() => <DialogSelectProvider />)
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
{language.t("dialog.provider.viewAll")}
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</Dialog>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Popover as Kobalte } from "@kobalte/core/popover"
|
|
2
|
+
import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
|
|
3
|
+
import { useLocal } from "@/context/local"
|
|
4
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
5
|
+
import { popularProviders } from "@/hooks/use-providers"
|
|
6
|
+
import { Button } from "@jonsoc/ui/button"
|
|
7
|
+
import { IconButton } from "@jonsoc/ui/icon-button"
|
|
8
|
+
import { Tag } from "@jonsoc/ui/tag"
|
|
9
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
10
|
+
import { List } from "@jonsoc/ui/list"
|
|
11
|
+
import { Tooltip } from "@jonsoc/ui/tooltip"
|
|
12
|
+
import { DialogSelectProvider } from "./dialog-select-provider"
|
|
13
|
+
import { DialogManageModels } from "./dialog-manage-models"
|
|
14
|
+
import { ModelTooltip } from "./model-tooltip"
|
|
15
|
+
import { useLanguage } from "@/context/language"
|
|
16
|
+
|
|
17
|
+
const ModelList: Component<{
|
|
18
|
+
provider?: string
|
|
19
|
+
class?: string
|
|
20
|
+
onSelect: () => void
|
|
21
|
+
action?: JSX.Element
|
|
22
|
+
}> = (props) => {
|
|
23
|
+
const local = useLocal()
|
|
24
|
+
const language = useLanguage()
|
|
25
|
+
|
|
26
|
+
const models = createMemo(() =>
|
|
27
|
+
local.model
|
|
28
|
+
.list()
|
|
29
|
+
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
|
|
30
|
+
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<List
|
|
35
|
+
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
|
36
|
+
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
|
|
37
|
+
emptyMessage={language.t("dialog.model.empty")}
|
|
38
|
+
key={(x) => `${x.provider.id}:${x.id}`}
|
|
39
|
+
items={models}
|
|
40
|
+
current={local.model.current()}
|
|
41
|
+
filterKeys={["provider.name", "name", "id"]}
|
|
42
|
+
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
|
43
|
+
groupBy={(x) => x.provider.name}
|
|
44
|
+
sortGroupsBy={(a, b) => {
|
|
45
|
+
const aProvider = a.items[0].provider.id
|
|
46
|
+
const bProvider = b.items[0].provider.id
|
|
47
|
+
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
|
48
|
+
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
|
49
|
+
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
|
50
|
+
}}
|
|
51
|
+
itemWrapper={(item, node) => (
|
|
52
|
+
<Tooltip
|
|
53
|
+
class="w-full"
|
|
54
|
+
placement="right-start"
|
|
55
|
+
gutter={12}
|
|
56
|
+
value={
|
|
57
|
+
<ModelTooltip
|
|
58
|
+
model={item}
|
|
59
|
+
latest={item.latest}
|
|
60
|
+
free={item.provider.id === "jonsoc" && (!item.cost || item.cost.input === 0)}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
{node}
|
|
65
|
+
</Tooltip>
|
|
66
|
+
)}
|
|
67
|
+
onSelect={(x) => {
|
|
68
|
+
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
|
69
|
+
recent: true,
|
|
70
|
+
})
|
|
71
|
+
props.onSelect()
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{(i) => (
|
|
75
|
+
<div class="w-full flex items-center gap-x-2 text-13-regular">
|
|
76
|
+
<span class="truncate">{i.name}</span>
|
|
77
|
+
<Show when={i.provider.id === "jonsoc" && (!i.cost || i.cost?.input === 0)}>
|
|
78
|
+
<Tag>{language.t("model.tag.free")}</Tag>
|
|
79
|
+
</Show>
|
|
80
|
+
<Show when={i.latest}>
|
|
81
|
+
<Tag>{language.t("model.tag.latest")}</Tag>
|
|
82
|
+
</Show>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</List>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
|
|
90
|
+
provider?: string
|
|
91
|
+
children?: JSX.Element
|
|
92
|
+
triggerAs?: T
|
|
93
|
+
triggerProps?: ComponentProps<T>
|
|
94
|
+
}) {
|
|
95
|
+
const [open, setOpen] = createSignal(false)
|
|
96
|
+
const dialog = useDialog()
|
|
97
|
+
|
|
98
|
+
const handleManage = () => {
|
|
99
|
+
setOpen(false)
|
|
100
|
+
dialog.show(() => <DialogManageModels />)
|
|
101
|
+
}
|
|
102
|
+
const language = useLanguage()
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
|
|
106
|
+
<Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
|
|
107
|
+
{props.children}
|
|
108
|
+
</Kobalte.Trigger>
|
|
109
|
+
<Kobalte.Portal>
|
|
110
|
+
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
|
|
111
|
+
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
|
|
112
|
+
<ModelList
|
|
113
|
+
provider={props.provider}
|
|
114
|
+
onSelect={() => setOpen(false)}
|
|
115
|
+
class="p-1"
|
|
116
|
+
action={
|
|
117
|
+
<IconButton
|
|
118
|
+
icon="sliders"
|
|
119
|
+
variant="ghost"
|
|
120
|
+
iconSize="normal"
|
|
121
|
+
class="size-6"
|
|
122
|
+
aria-label={language.t("dialog.model.manage")}
|
|
123
|
+
title={language.t("dialog.model.manage")}
|
|
124
|
+
onClick={handleManage}
|
|
125
|
+
/>
|
|
126
|
+
}
|
|
127
|
+
/>
|
|
128
|
+
</Kobalte.Content>
|
|
129
|
+
</Kobalte.Portal>
|
|
130
|
+
</Kobalte>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
|
135
|
+
const dialog = useDialog()
|
|
136
|
+
const language = useLanguage()
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Dialog
|
|
140
|
+
title={language.t("dialog.model.select.title")}
|
|
141
|
+
action={
|
|
142
|
+
<Button
|
|
143
|
+
class="h-7 -my-1 text-14-medium"
|
|
144
|
+
icon="plus-small"
|
|
145
|
+
tabIndex={-1}
|
|
146
|
+
onClick={() => dialog.show(() => <DialogSelectProvider />)}
|
|
147
|
+
>
|
|
148
|
+
{language.t("command.provider.connect")}
|
|
149
|
+
</Button>
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
<ModelList provider={props.provider} onSelect={() => dialog.close()} />
|
|
153
|
+
<Button
|
|
154
|
+
variant="ghost"
|
|
155
|
+
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
|
156
|
+
onClick={() => dialog.show(() => <DialogManageModels />)}
|
|
157
|
+
>
|
|
158
|
+
{language.t("dialog.model.manage")}
|
|
159
|
+
</Button>
|
|
160
|
+
</Dialog>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Component, Show } from "solid-js"
|
|
2
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
3
|
+
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
4
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
5
|
+
import { List } from "@jonsoc/ui/list"
|
|
6
|
+
import { Tag } from "@jonsoc/ui/tag"
|
|
7
|
+
import { ProviderIcon } from "@jonsoc/ui/provider-icon"
|
|
8
|
+
import { IconName } from "@jonsoc/ui/icons/provider"
|
|
9
|
+
import { DialogConnectProvider } from "./dialog-connect-provider"
|
|
10
|
+
import { useLanguage } from "@/context/language"
|
|
11
|
+
|
|
12
|
+
export const DialogSelectProvider: Component = () => {
|
|
13
|
+
const dialog = useDialog()
|
|
14
|
+
const providers = useProviders()
|
|
15
|
+
const language = useLanguage()
|
|
16
|
+
|
|
17
|
+
const popularGroup = () => language.t("dialog.provider.group.popular")
|
|
18
|
+
const otherGroup = () => language.t("dialog.provider.group.other")
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Dialog title={language.t("command.provider.connect")}>
|
|
22
|
+
<List
|
|
23
|
+
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
|
|
24
|
+
emptyMessage={language.t("dialog.provider.empty")}
|
|
25
|
+
activeIcon="plus-small"
|
|
26
|
+
key={(x) => x?.id}
|
|
27
|
+
items={() => {
|
|
28
|
+
language.locale()
|
|
29
|
+
return providers.all()
|
|
30
|
+
}}
|
|
31
|
+
filterKeys={["id", "name"]}
|
|
32
|
+
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
|
33
|
+
sortBy={(a, b) => {
|
|
34
|
+
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
|
35
|
+
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
|
36
|
+
return a.name.localeCompare(b.name)
|
|
37
|
+
}}
|
|
38
|
+
sortGroupsBy={(a, b) => {
|
|
39
|
+
const popular = popularGroup()
|
|
40
|
+
if (a.category === popular && b.category !== popular) return -1
|
|
41
|
+
if (b.category === popular && a.category !== popular) return 1
|
|
42
|
+
return 0
|
|
43
|
+
}}
|
|
44
|
+
onSelect={(x) => {
|
|
45
|
+
if (!x) return
|
|
46
|
+
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{(i) => (
|
|
50
|
+
<div class="px-1.25 w-full flex items-center gap-x-3">
|
|
51
|
+
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
|
52
|
+
<span>{i.name}</span>
|
|
53
|
+
<Show when={i.id === "jonsoc"}>
|
|
54
|
+
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
|
55
|
+
</Show>
|
|
56
|
+
<Show when={i.id === "anthropic"}>
|
|
57
|
+
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
|
58
|
+
</Show>
|
|
59
|
+
<Show when={i.id === "openai"}>
|
|
60
|
+
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
|
61
|
+
</Show>
|
|
62
|
+
<Show when={i.id.startsWith("github-copilot")}>
|
|
63
|
+
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
|
64
|
+
</Show>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</List>
|
|
68
|
+
</Dialog>
|
|
69
|
+
)
|
|
70
|
+
}
|