@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,259 @@
|
|
|
1
|
+
import { Button } from "@jonsoc/ui/button"
|
|
2
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
3
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
4
|
+
import { TextField } from "@jonsoc/ui/text-field"
|
|
5
|
+
import { Icon } from "@jonsoc/ui/icon"
|
|
6
|
+
import { createMemo, createSignal, For, Show } from "solid-js"
|
|
7
|
+
import { createStore } from "solid-js/store"
|
|
8
|
+
import { useGlobalSDK } from "@/context/global-sdk"
|
|
9
|
+
import { useGlobalSync } from "@/context/global-sync"
|
|
10
|
+
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
|
11
|
+
import { getFilename } from "@jonsoc/util/path"
|
|
12
|
+
import { Avatar } from "@jonsoc/ui/avatar"
|
|
13
|
+
import { useLanguage } from "@/context/language"
|
|
14
|
+
|
|
15
|
+
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
|
16
|
+
|
|
17
|
+
export function DialogEditProject(props: { project: LocalProject }) {
|
|
18
|
+
const dialog = useDialog()
|
|
19
|
+
const globalSDK = useGlobalSDK()
|
|
20
|
+
const globalSync = useGlobalSync()
|
|
21
|
+
const language = useLanguage()
|
|
22
|
+
|
|
23
|
+
const folderName = createMemo(() => getFilename(props.project.worktree))
|
|
24
|
+
const defaultName = createMemo(() => props.project.name || folderName())
|
|
25
|
+
|
|
26
|
+
const [store, setStore] = createStore({
|
|
27
|
+
name: defaultName(),
|
|
28
|
+
color: props.project.icon?.color || "pink",
|
|
29
|
+
iconUrl: props.project.icon?.override || "",
|
|
30
|
+
startup: props.project.commands?.start ?? "",
|
|
31
|
+
saving: false,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const [dragOver, setDragOver] = createSignal(false)
|
|
35
|
+
const [iconHover, setIconHover] = createSignal(false)
|
|
36
|
+
|
|
37
|
+
function handleFileSelect(file: File) {
|
|
38
|
+
if (!file.type.startsWith("image/")) return
|
|
39
|
+
const reader = new FileReader()
|
|
40
|
+
reader.onload = (e) => {
|
|
41
|
+
setStore("iconUrl", e.target?.result as string)
|
|
42
|
+
setIconHover(false)
|
|
43
|
+
}
|
|
44
|
+
reader.readAsDataURL(file)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleDrop(e: DragEvent) {
|
|
48
|
+
e.preventDefault()
|
|
49
|
+
setDragOver(false)
|
|
50
|
+
const file = e.dataTransfer?.files[0]
|
|
51
|
+
if (file) handleFileSelect(file)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleDragOver(e: DragEvent) {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
setDragOver(true)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleDragLeave() {
|
|
60
|
+
setDragOver(false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleInputChange(e: Event) {
|
|
64
|
+
const input = e.target as HTMLInputElement
|
|
65
|
+
const file = input.files?.[0]
|
|
66
|
+
if (file) handleFileSelect(file)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clearIcon() {
|
|
70
|
+
setStore("iconUrl", "")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function handleSubmit(e: SubmitEvent) {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
|
|
76
|
+
setStore("saving", true)
|
|
77
|
+
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
|
78
|
+
const start = store.startup.trim()
|
|
79
|
+
|
|
80
|
+
if (props.project.id && props.project.id !== "global") {
|
|
81
|
+
await globalSDK.client.project.update({
|
|
82
|
+
projectID: props.project.id,
|
|
83
|
+
directory: props.project.worktree,
|
|
84
|
+
name,
|
|
85
|
+
icon: { color: store.color, override: store.iconUrl },
|
|
86
|
+
commands: { start },
|
|
87
|
+
})
|
|
88
|
+
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
|
89
|
+
setStore("saving", false)
|
|
90
|
+
dialog.close()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
globalSync.project.meta(props.project.worktree, {
|
|
95
|
+
name,
|
|
96
|
+
icon: { color: store.color, override: store.iconUrl || undefined },
|
|
97
|
+
commands: { start: start || undefined },
|
|
98
|
+
})
|
|
99
|
+
setStore("saving", false)
|
|
100
|
+
dialog.close()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
|
|
105
|
+
<form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
|
|
106
|
+
<div class="flex flex-col gap-4">
|
|
107
|
+
<TextField
|
|
108
|
+
autofocus
|
|
109
|
+
type="text"
|
|
110
|
+
label={language.t("dialog.project.edit.name")}
|
|
111
|
+
placeholder={folderName()}
|
|
112
|
+
value={store.name}
|
|
113
|
+
onChange={(v) => setStore("name", v)}
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<div class="flex flex-col gap-2">
|
|
117
|
+
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
|
118
|
+
<div class="flex gap-3 items-start">
|
|
119
|
+
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
|
|
120
|
+
<div
|
|
121
|
+
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
|
122
|
+
classList={{
|
|
123
|
+
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
|
124
|
+
"border-border-base hover:border-border-strong": !dragOver(),
|
|
125
|
+
"overflow-hidden": !!store.iconUrl,
|
|
126
|
+
}}
|
|
127
|
+
onDrop={handleDrop}
|
|
128
|
+
onDragOver={handleDragOver}
|
|
129
|
+
onDragLeave={handleDragLeave}
|
|
130
|
+
onClick={() => {
|
|
131
|
+
if (store.iconUrl && iconHover()) {
|
|
132
|
+
clearIcon()
|
|
133
|
+
} else {
|
|
134
|
+
document.getElementById("icon-upload")?.click()
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<Show
|
|
139
|
+
when={store.iconUrl}
|
|
140
|
+
fallback={
|
|
141
|
+
<div class="size-full flex items-center justify-center">
|
|
142
|
+
<Avatar
|
|
143
|
+
fallback={store.name || defaultName()}
|
|
144
|
+
{...getAvatarColors(store.color)}
|
|
145
|
+
class="size-full"
|
|
146
|
+
style={{ "font-size": "32px" }}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
<img
|
|
152
|
+
src={store.iconUrl}
|
|
153
|
+
alt={language.t("dialog.project.edit.icon.alt")}
|
|
154
|
+
class="size-full object-cover"
|
|
155
|
+
/>
|
|
156
|
+
</Show>
|
|
157
|
+
</div>
|
|
158
|
+
<div
|
|
159
|
+
style={{
|
|
160
|
+
position: "absolute",
|
|
161
|
+
top: 0,
|
|
162
|
+
left: 0,
|
|
163
|
+
width: "64px",
|
|
164
|
+
height: "64px",
|
|
165
|
+
background: "rgba(0,0,0,0.6)",
|
|
166
|
+
"border-radius": "6px",
|
|
167
|
+
"z-index": 10,
|
|
168
|
+
"pointer-events": "none",
|
|
169
|
+
opacity: iconHover() && !store.iconUrl ? 1 : 0,
|
|
170
|
+
display: "flex",
|
|
171
|
+
"align-items": "center",
|
|
172
|
+
"justify-content": "center",
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
|
|
176
|
+
</div>
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
position: "absolute",
|
|
180
|
+
top: 0,
|
|
181
|
+
left: 0,
|
|
182
|
+
width: "64px",
|
|
183
|
+
height: "64px",
|
|
184
|
+
background: "rgba(0,0,0,0.6)",
|
|
185
|
+
"border-radius": "6px",
|
|
186
|
+
"z-index": 10,
|
|
187
|
+
"pointer-events": "none",
|
|
188
|
+
opacity: iconHover() && store.iconUrl ? 1 : 0,
|
|
189
|
+
display: "flex",
|
|
190
|
+
"align-items": "center",
|
|
191
|
+
"justify-content": "center",
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<Icon name="trash" size="large" class="text-icon-invert-base" />
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
|
198
|
+
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
|
|
199
|
+
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
|
200
|
+
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<Show when={!store.iconUrl}>
|
|
206
|
+
<div class="flex flex-col gap-2">
|
|
207
|
+
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
|
208
|
+
<div class="flex gap-1.5">
|
|
209
|
+
<For each={AVATAR_COLOR_KEYS}>
|
|
210
|
+
{(color) => (
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
aria-label={language.t("dialog.project.edit.color.select", { color })}
|
|
214
|
+
aria-pressed={store.color === color}
|
|
215
|
+
classList={{
|
|
216
|
+
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
|
217
|
+
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
|
218
|
+
store.color === color,
|
|
219
|
+
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
|
220
|
+
store.color !== color,
|
|
221
|
+
}}
|
|
222
|
+
onClick={() => setStore("color", color)}
|
|
223
|
+
>
|
|
224
|
+
<Avatar
|
|
225
|
+
fallback={store.name || defaultName()}
|
|
226
|
+
{...getAvatarColors(color)}
|
|
227
|
+
class="size-full rounded"
|
|
228
|
+
/>
|
|
229
|
+
</button>
|
|
230
|
+
)}
|
|
231
|
+
</For>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</Show>
|
|
235
|
+
|
|
236
|
+
<TextField
|
|
237
|
+
multiline
|
|
238
|
+
label={language.t("dialog.project.edit.worktree.startup")}
|
|
239
|
+
description={language.t("dialog.project.edit.worktree.startup.description")}
|
|
240
|
+
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
|
241
|
+
value={store.startup}
|
|
242
|
+
onChange={(v) => setStore("startup", v)}
|
|
243
|
+
spellcheck={false}
|
|
244
|
+
class="max-h-40 w-full font-mono text-xs no-scrollbar"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div class="flex justify-end gap-2">
|
|
249
|
+
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
|
250
|
+
{language.t("common.cancel")}
|
|
251
|
+
</Button>
|
|
252
|
+
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
|
253
|
+
{store.saving ? language.t("common.saving") : language.t("common.save")}
|
|
254
|
+
</Button>
|
|
255
|
+
</div>
|
|
256
|
+
</form>
|
|
257
|
+
</Dialog>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Component, createMemo } from "solid-js"
|
|
2
|
+
import { useNavigate, useParams } from "@solidjs/router"
|
|
3
|
+
import { useSync } from "@/context/sync"
|
|
4
|
+
import { useSDK } from "@/context/sdk"
|
|
5
|
+
import { usePrompt } from "@/context/prompt"
|
|
6
|
+
import { useDialog } from "@jonsoc/ui/context/dialog"
|
|
7
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
8
|
+
import { List } from "@jonsoc/ui/list"
|
|
9
|
+
import { extractPromptFromParts } from "@/utils/prompt"
|
|
10
|
+
import type { TextPart as SDKTextPart } from "@jonsoc/sdk/v2/client"
|
|
11
|
+
import { base64Encode } from "@jonsoc/util/encode"
|
|
12
|
+
import { useLanguage } from "@/context/language"
|
|
13
|
+
|
|
14
|
+
interface ForkableMessage {
|
|
15
|
+
id: string
|
|
16
|
+
text: string
|
|
17
|
+
time: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatTime(date: Date): string {
|
|
21
|
+
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DialogFork: Component = () => {
|
|
25
|
+
const params = useParams()
|
|
26
|
+
const navigate = useNavigate()
|
|
27
|
+
const sync = useSync()
|
|
28
|
+
const sdk = useSDK()
|
|
29
|
+
const prompt = usePrompt()
|
|
30
|
+
const dialog = useDialog()
|
|
31
|
+
const language = useLanguage()
|
|
32
|
+
|
|
33
|
+
const messages = createMemo((): ForkableMessage[] => {
|
|
34
|
+
const sessionID = params.id
|
|
35
|
+
if (!sessionID) return []
|
|
36
|
+
|
|
37
|
+
const msgs = sync.data.message[sessionID] ?? []
|
|
38
|
+
const result: ForkableMessage[] = []
|
|
39
|
+
|
|
40
|
+
for (const message of msgs) {
|
|
41
|
+
if (message.role !== "user") continue
|
|
42
|
+
|
|
43
|
+
const parts = sync.data.part[message.id] ?? []
|
|
44
|
+
const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
|
|
45
|
+
if (!textPart) continue
|
|
46
|
+
|
|
47
|
+
result.push({
|
|
48
|
+
id: message.id,
|
|
49
|
+
text: textPart.text.replace(/\n/g, " ").slice(0, 200),
|
|
50
|
+
time: formatTime(new Date(message.time.created)),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result.reverse()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const handleSelect = (item: ForkableMessage | undefined) => {
|
|
58
|
+
if (!item) return
|
|
59
|
+
|
|
60
|
+
const sessionID = params.id
|
|
61
|
+
if (!sessionID) return
|
|
62
|
+
|
|
63
|
+
const parts = sync.data.part[item.id] ?? []
|
|
64
|
+
const restored = extractPromptFromParts(parts, {
|
|
65
|
+
directory: sdk.directory,
|
|
66
|
+
attachmentName: language.t("common.attachment"),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
dialog.close()
|
|
70
|
+
|
|
71
|
+
sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
|
|
72
|
+
if (!forked.data) return
|
|
73
|
+
navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
prompt.set(restored)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Dialog title={language.t("command.session.fork")}>
|
|
82
|
+
<List
|
|
83
|
+
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
|
84
|
+
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
|
85
|
+
emptyMessage={language.t("dialog.fork.empty")}
|
|
86
|
+
key={(x) => x.id}
|
|
87
|
+
items={messages}
|
|
88
|
+
filterKeys={["text"]}
|
|
89
|
+
onSelect={handleSelect}
|
|
90
|
+
>
|
|
91
|
+
{(item) => (
|
|
92
|
+
<div class="w-full flex items-center gap-2">
|
|
93
|
+
<span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
|
|
94
|
+
{item.text}
|
|
95
|
+
</span>
|
|
96
|
+
<span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
|
|
97
|
+
{item.time}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</List>
|
|
102
|
+
</Dialog>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Dialog } from "@jonsoc/ui/dialog"
|
|
2
|
+
import { List } from "@jonsoc/ui/list"
|
|
3
|
+
import { Switch } from "@jonsoc/ui/switch"
|
|
4
|
+
import type { Component } from "solid-js"
|
|
5
|
+
import { useLocal } from "@/context/local"
|
|
6
|
+
import { popularProviders } from "@/hooks/use-providers"
|
|
7
|
+
import { useLanguage } from "@/context/language"
|
|
8
|
+
|
|
9
|
+
export const DialogManageModels: Component = () => {
|
|
10
|
+
const local = useLocal()
|
|
11
|
+
const language = useLanguage()
|
|
12
|
+
return (
|
|
13
|
+
<Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
|
|
14
|
+
<List
|
|
15
|
+
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
|
|
16
|
+
emptyMessage={language.t("dialog.model.empty")}
|
|
17
|
+
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
|
18
|
+
items={local.model.list()}
|
|
19
|
+
filterKeys={["provider.name", "name", "id"]}
|
|
20
|
+
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
|
21
|
+
groupBy={(x) => x.provider.name}
|
|
22
|
+
sortGroupsBy={(a, b) => {
|
|
23
|
+
const aProvider = a.items[0].provider.id
|
|
24
|
+
const bProvider = b.items[0].provider.id
|
|
25
|
+
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
|
26
|
+
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
|
27
|
+
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
|
28
|
+
}}
|
|
29
|
+
onSelect={(x) => {
|
|
30
|
+
if (!x) return
|
|
31
|
+
const visible = local.model.visible({
|
|
32
|
+
modelID: x.id,
|
|
33
|
+
providerID: x.provider.id,
|
|
34
|
+
})
|
|
35
|
+
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{(i) => (
|
|
39
|
+
<div class="w-full flex items-center justify-between gap-x-3">
|
|
40
|
+
<span>{i.name}</span>
|
|
41
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
42
|
+
<Switch
|
|
43
|
+
checked={
|
|
44
|
+
!!local.model.visible({
|
|
45
|
+
modelID: i.id,
|
|
46
|
+
providerID: i.provider.id,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
onChange={(checked) => {
|
|
50
|
+
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</List>
|
|
57
|
+
</Dialog>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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 { List } from "@jonsoc/ui/list"
|
|
5
|
+
import { getDirectory, getFilename } from "@jonsoc/util/path"
|
|
6
|
+
import fuzzysort from "fuzzysort"
|
|
7
|
+
import { createMemo } from "solid-js"
|
|
8
|
+
import { useGlobalSDK } from "@/context/global-sdk"
|
|
9
|
+
import { useGlobalSync } from "@/context/global-sync"
|
|
10
|
+
import { useLanguage } from "@/context/language"
|
|
11
|
+
|
|
12
|
+
interface DialogSelectDirectoryProps {
|
|
13
|
+
title?: string
|
|
14
|
+
multiple?: boolean
|
|
15
|
+
onSelect: (result: string | string[] | null) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|
19
|
+
const sync = useGlobalSync()
|
|
20
|
+
const sdk = useGlobalSDK()
|
|
21
|
+
const dialog = useDialog()
|
|
22
|
+
const language = useLanguage()
|
|
23
|
+
|
|
24
|
+
const home = createMemo(() => sync.data.path.home)
|
|
25
|
+
|
|
26
|
+
const start = createMemo(() => sync.data.path.home || sync.data.path.directory)
|
|
27
|
+
|
|
28
|
+
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
|
29
|
+
|
|
30
|
+
function normalize(input: string) {
|
|
31
|
+
const v = input.replaceAll("\\", "/")
|
|
32
|
+
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
|
33
|
+
return v.replace(/\/+/g, "/")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeDriveRoot(input: string) {
|
|
37
|
+
const v = normalize(input)
|
|
38
|
+
if (/^[A-Za-z]:$/.test(v)) return v + "/"
|
|
39
|
+
return v
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function trimTrailing(input: string) {
|
|
43
|
+
const v = normalizeDriveRoot(input)
|
|
44
|
+
if (v === "/") return v
|
|
45
|
+
if (v === "//") return v
|
|
46
|
+
if (/^[A-Za-z]:\/$/.test(v)) return v
|
|
47
|
+
return v.replace(/\/+$/, "")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function join(base: string | undefined, rel: string) {
|
|
51
|
+
const b = trimTrailing(base ?? "")
|
|
52
|
+
const r = trimTrailing(rel).replace(/^\/+/, "")
|
|
53
|
+
if (!b) return r
|
|
54
|
+
if (!r) return b
|
|
55
|
+
if (b.endsWith("/")) return b + r
|
|
56
|
+
return b + "/" + r
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rootOf(input: string) {
|
|
60
|
+
const v = normalizeDriveRoot(input)
|
|
61
|
+
if (v.startsWith("//")) return "//"
|
|
62
|
+
if (v.startsWith("/")) return "/"
|
|
63
|
+
if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3)
|
|
64
|
+
return ""
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function display(path: string) {
|
|
68
|
+
const full = trimTrailing(path)
|
|
69
|
+
const h = home()
|
|
70
|
+
if (!h) return full
|
|
71
|
+
|
|
72
|
+
const hn = trimTrailing(h)
|
|
73
|
+
const lc = full.toLowerCase()
|
|
74
|
+
const hc = hn.toLowerCase()
|
|
75
|
+
if (lc === hc) return "~"
|
|
76
|
+
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
|
77
|
+
return full
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function scoped(filter: string) {
|
|
81
|
+
const base = start()
|
|
82
|
+
if (!base) return
|
|
83
|
+
|
|
84
|
+
const raw = normalizeDriveRoot(filter.trim())
|
|
85
|
+
if (!raw) return { directory: trimTrailing(base), path: "" }
|
|
86
|
+
|
|
87
|
+
const h = home()
|
|
88
|
+
if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" }
|
|
89
|
+
if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) }
|
|
90
|
+
|
|
91
|
+
const root = rootOf(raw)
|
|
92
|
+
if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) }
|
|
93
|
+
return { directory: trimTrailing(base), path: raw }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function dirs(dir: string) {
|
|
97
|
+
const key = trimTrailing(dir)
|
|
98
|
+
const existing = cache.get(key)
|
|
99
|
+
if (existing) return existing
|
|
100
|
+
|
|
101
|
+
const request = sdk.client.file
|
|
102
|
+
.list({ directory: key, path: "" })
|
|
103
|
+
.then((x) => x.data ?? [])
|
|
104
|
+
.catch(() => [])
|
|
105
|
+
.then((nodes) =>
|
|
106
|
+
nodes
|
|
107
|
+
.filter((n) => n.type === "directory")
|
|
108
|
+
.map((n) => ({
|
|
109
|
+
name: n.name,
|
|
110
|
+
absolute: trimTrailing(normalizeDriveRoot(n.absolute)),
|
|
111
|
+
})),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
cache.set(key, request)
|
|
115
|
+
return request
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function match(dir: string, query: string, limit: number) {
|
|
119
|
+
const items = await dirs(dir)
|
|
120
|
+
if (!query) return items.slice(0, limit).map((x) => x.absolute)
|
|
121
|
+
return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const directories = async (filter: string) => {
|
|
125
|
+
const input = scoped(filter)
|
|
126
|
+
if (!input) return [] as string[]
|
|
127
|
+
|
|
128
|
+
const raw = normalizeDriveRoot(filter.trim())
|
|
129
|
+
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
|
130
|
+
|
|
131
|
+
const query = normalizeDriveRoot(input.path)
|
|
132
|
+
|
|
133
|
+
if (!isPath) {
|
|
134
|
+
const results = await sdk.client.find
|
|
135
|
+
.files({ directory: input.directory, query, type: "directory", limit: 50 })
|
|
136
|
+
.then((x) => x.data ?? [])
|
|
137
|
+
.catch(() => [])
|
|
138
|
+
|
|
139
|
+
return results.map((rel) => join(input.directory, rel)).slice(0, 50)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const segments = query.replace(/^\/+/, "").split("/")
|
|
143
|
+
const head = segments.slice(0, segments.length - 1).filter((x) => x && x !== ".")
|
|
144
|
+
const tail = segments[segments.length - 1] ?? ""
|
|
145
|
+
|
|
146
|
+
const cap = 12
|
|
147
|
+
const branch = 4
|
|
148
|
+
let paths = [input.directory]
|
|
149
|
+
for (const part of head) {
|
|
150
|
+
if (part === "..") {
|
|
151
|
+
paths = paths.map((p) => {
|
|
152
|
+
const v = trimTrailing(p)
|
|
153
|
+
if (v === "/") return v
|
|
154
|
+
if (/^[A-Za-z]:\/$/.test(v)) return v
|
|
155
|
+
const i = v.lastIndexOf("/")
|
|
156
|
+
if (i <= 0) return "/"
|
|
157
|
+
return v.slice(0, i)
|
|
158
|
+
})
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat()
|
|
163
|
+
paths = Array.from(new Set(next)).slice(0, cap)
|
|
164
|
+
if (paths.length === 0) return [] as string[]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
|
168
|
+
return Array.from(new Set(out)).slice(0, 50)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolve(absolute: string) {
|
|
172
|
+
props.onSelect(props.multiple ? [absolute] : absolute)
|
|
173
|
+
dialog.close()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Dialog title={props.title ?? language.t("command.project.open")}>
|
|
178
|
+
<List
|
|
179
|
+
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
|
|
180
|
+
emptyMessage={language.t("dialog.directory.empty")}
|
|
181
|
+
loadingMessage={language.t("common.loading")}
|
|
182
|
+
items={directories}
|
|
183
|
+
key={(x) => x}
|
|
184
|
+
onSelect={(path) => {
|
|
185
|
+
if (!path) return
|
|
186
|
+
resolve(path)
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{(absolute) => {
|
|
190
|
+
const path = display(absolute)
|
|
191
|
+
return (
|
|
192
|
+
<div class="w-full flex items-center justify-between rounded-md">
|
|
193
|
+
<div class="flex items-center gap-x-3 grow min-w-0">
|
|
194
|
+
<FileIcon node={{ path: absolute, type: "directory" }} class="shrink-0 size-4" />
|
|
195
|
+
<div class="flex items-center text-14-regular min-w-0">
|
|
196
|
+
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
|
197
|
+
{getDirectory(path)}
|
|
198
|
+
</span>
|
|
199
|
+
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}}
|
|
205
|
+
</List>
|
|
206
|
+
</Dialog>
|
|
207
|
+
)
|
|
208
|
+
}
|