saeeol 1.2.2 → 1.2.4
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/bin/saeeol.cjs +203 -0
- package/npm/bin/saeeol +0 -0
- package/package.json +2 -2
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +1 -1
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +5 -5
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +2 -2
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +3 -3
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +4 -4
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +4 -4
- package/src/cli/cmd/tui/context/app/args.tsx +15 -0
- package/src/cli/cmd/tui/context/app/directory.ts +15 -0
- package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
- package/src/cli/cmd/tui/context/app/editor.ts +425 -0
- package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/app/project.tsx +109 -0
- package/src/cli/cmd/tui/context/app/route.tsx +67 -0
- package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
- package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
- package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
- package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/context/args.tsx +1 -15
- package/src/cli/cmd/tui/context/directory.ts +1 -15
- package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
- package/src/cli/cmd/tui/context/editor.ts +1 -425
- package/src/cli/cmd/tui/context/event.ts +1 -45
- package/src/cli/cmd/tui/context/exit.tsx +1 -67
- package/src/cli/cmd/tui/context/helper.tsx +1 -25
- package/src/cli/cmd/tui/context/keybind.tsx +1 -105
- package/src/cli/cmd/tui/context/kv.tsx +1 -76
- package/src/cli/cmd/tui/context/local.tsx +1 -478
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
- package/src/cli/cmd/tui/context/project.tsx +1 -109
- package/src/cli/cmd/tui/context/prompt.tsx +1 -18
- package/src/cli/cmd/tui/context/route.tsx +1 -67
- package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
- package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
- package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
- package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/sdk.tsx +1 -142
- package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/sync.tsx +1 -713
- package/src/cli/cmd/tui/context/theme.tsx +1 -307
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
- package/src/ltm/pipeline.ts +103 -1
- package/test/server/contract.test.ts +249 -0
|
@@ -1,76 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Filesystem } from "@/util/filesystem"
|
|
3
|
-
import { Flock } from "@saeeol/core/util/flock"
|
|
4
|
-
import { rename, rm } from "fs/promises"
|
|
5
|
-
import { createSignal, type Setter } from "solid-js"
|
|
6
|
-
import { createStore, unwrap } from "solid-js/store"
|
|
7
|
-
import { createSimpleContext } from "./helper"
|
|
8
|
-
import path from "path"
|
|
9
|
-
|
|
10
|
-
export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
|
11
|
-
name: "KV",
|
|
12
|
-
init: () => {
|
|
13
|
-
const [ready, setReady] = createSignal(false)
|
|
14
|
-
const [store, setStore] = createStore<Record<string, any>>()
|
|
15
|
-
const filePath = path.join(Global.Path.state, "kv.json")
|
|
16
|
-
const lock = `tui-kv:${filePath}`
|
|
17
|
-
// Queue same-process writes so rapid updates persist in order.
|
|
18
|
-
let write = Promise.resolve()
|
|
19
|
-
|
|
20
|
-
// Write to a temp file first so kv.json is only replaced once the JSON is complete, avoiding partial writes if shutdown interrupts persistence.
|
|
21
|
-
function writeSnapshot(snapshot: Record<string, any>) {
|
|
22
|
-
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`
|
|
23
|
-
return Filesystem.writeJson(tempPath, snapshot)
|
|
24
|
-
.then(() => rename(tempPath, filePath))
|
|
25
|
-
.catch(async (error) => {
|
|
26
|
-
await rm(tempPath, { force: true }).catch(() => undefined)
|
|
27
|
-
throw error
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Read under the same lock used for writes because kv.json is shared across processes.
|
|
32
|
-
Flock.withLock(lock, () => Filesystem.readJson<Record<string, any>>(filePath))
|
|
33
|
-
.then((x) => {
|
|
34
|
-
setStore(x)
|
|
35
|
-
})
|
|
36
|
-
.catch((error) => {
|
|
37
|
-
console.error("Failed to read KV state", { filePath, error })
|
|
38
|
-
})
|
|
39
|
-
.finally(() => {
|
|
40
|
-
setReady(true)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const result = {
|
|
44
|
-
get ready() {
|
|
45
|
-
return ready()
|
|
46
|
-
},
|
|
47
|
-
get store() {
|
|
48
|
-
return store
|
|
49
|
-
},
|
|
50
|
-
signal<T>(name: string, defaultValue: T) {
|
|
51
|
-
if (store[name] === undefined) setStore(name, defaultValue)
|
|
52
|
-
return [
|
|
53
|
-
function () {
|
|
54
|
-
return result.get(name)
|
|
55
|
-
},
|
|
56
|
-
function setter(next: Setter<T>) {
|
|
57
|
-
result.set(name, next)
|
|
58
|
-
},
|
|
59
|
-
] as const
|
|
60
|
-
},
|
|
61
|
-
get(key: string, defaultValue?: any) {
|
|
62
|
-
return store[key] ?? defaultValue
|
|
63
|
-
},
|
|
64
|
-
set(key: string, value: any) {
|
|
65
|
-
setStore(key, value)
|
|
66
|
-
const snapshot = structuredClone(unwrap(store))
|
|
67
|
-
write = write
|
|
68
|
-
.then(() => Flock.withLock(lock, () => writeSnapshot(snapshot)))
|
|
69
|
-
.catch((error) => {
|
|
70
|
-
console.error("Failed to write KV state", { filePath, error })
|
|
71
|
-
})
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
return result
|
|
75
|
-
},
|
|
76
|
-
})
|
|
1
|
+
export * from "./runtime/kv"
|
|
@@ -1,478 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createSimpleContext } from "./helper"
|
|
3
|
-
import { batch, createEffect, createMemo } from "solid-js"
|
|
4
|
-
import { useSync } from "@tui/context/sync"
|
|
5
|
-
import { useTheme } from "@tui/context/theme"
|
|
6
|
-
import { uniqueBy } from "remeda"
|
|
7
|
-
import path from "path"
|
|
8
|
-
import { Global } from "@saeeol/core/global"
|
|
9
|
-
import { iife } from "@/util/iife"
|
|
10
|
-
import { useToast } from "../ui/toast"
|
|
11
|
-
import { useArgs } from "./args"
|
|
12
|
-
import { useSDK } from "./sdk"
|
|
13
|
-
import { useProject } from "./project"
|
|
14
|
-
import { RGBA } from "@opentui/core"
|
|
15
|
-
import { Filesystem } from "@/util/filesystem"
|
|
16
|
-
|
|
17
|
-
export function parseModel(model: string) {
|
|
18
|
-
const [providerID, ...rest] = model.split("/")
|
|
19
|
-
return {
|
|
20
|
-
providerID: providerID,
|
|
21
|
-
modelID: rest.join("/"),
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
26
|
-
name: "Local",
|
|
27
|
-
init: () => {
|
|
28
|
-
const sync = useSync()
|
|
29
|
-
const sdk = useSDK()
|
|
30
|
-
const project = useProject()
|
|
31
|
-
const toast = useToast()
|
|
32
|
-
|
|
33
|
-
function isModelValid(model: { providerID: string; modelID: string }) {
|
|
34
|
-
const provider = sync.data.provider.find((x) => x.id === model.providerID)
|
|
35
|
-
return !!provider?.models?.[model.modelID]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
|
|
39
|
-
for (const modelFn of modelFns) {
|
|
40
|
-
const model = modelFn()
|
|
41
|
-
if (!model) continue
|
|
42
|
-
if (isModelValid(model)) return model
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const agent = iife(() => {
|
|
47
|
-
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
|
48
|
-
const visibleAgents = createMemo(() => sync.data.agent.filter((x) => !x.hidden))
|
|
49
|
-
const [agentStore, setAgentStore] = createStore({
|
|
50
|
-
current: undefined as string | undefined,
|
|
51
|
-
})
|
|
52
|
-
const { theme } = useTheme()
|
|
53
|
-
const colors = createMemo(() => [
|
|
54
|
-
theme.secondary,
|
|
55
|
-
theme.accent,
|
|
56
|
-
theme.success,
|
|
57
|
-
theme.warning,
|
|
58
|
-
theme.primary,
|
|
59
|
-
theme.error,
|
|
60
|
-
theme.info,
|
|
61
|
-
])
|
|
62
|
-
return {
|
|
63
|
-
list() {
|
|
64
|
-
return agents()
|
|
65
|
-
},
|
|
66
|
-
current() {
|
|
67
|
-
const found = agents().find((x) => x.name === agentStore.current)
|
|
68
|
-
if (found) return found
|
|
69
|
-
const fallback = agents().at(0)
|
|
70
|
-
if (fallback) setAgentStore("current", fallback.name)
|
|
71
|
-
return fallback
|
|
72
|
-
},
|
|
73
|
-
set(name: string) {
|
|
74
|
-
if (!agents().some((x) => x.name === name))
|
|
75
|
-
return toast.show({
|
|
76
|
-
variant: "warning",
|
|
77
|
-
message: `Agent not found: ${name}`,
|
|
78
|
-
duration: 3000,
|
|
79
|
-
})
|
|
80
|
-
setAgentStore("current", name)
|
|
81
|
-
},
|
|
82
|
-
move(direction: 1 | -1) {
|
|
83
|
-
batch(() => {
|
|
84
|
-
const current = this.current()
|
|
85
|
-
if (!current) return
|
|
86
|
-
let next = agents().findIndex((x) => x.name === current.name) + direction
|
|
87
|
-
if (next < 0) next = agents().length - 1
|
|
88
|
-
if (next >= agents().length) next = 0
|
|
89
|
-
const value = agents()[next]
|
|
90
|
-
if (!value) return
|
|
91
|
-
setAgentStore("current", value.name)
|
|
92
|
-
})
|
|
93
|
-
},
|
|
94
|
-
color(name: string) {
|
|
95
|
-
const index = visibleAgents().findIndex((x) => x.name === name)
|
|
96
|
-
if (index === -1) return colors()[0]
|
|
97
|
-
const agent = visibleAgents()[index]
|
|
98
|
-
|
|
99
|
-
if (agent?.color) {
|
|
100
|
-
const color = agent.color
|
|
101
|
-
if (color.startsWith("#")) return RGBA.fromHex(color)
|
|
102
|
-
// already validated by config, just satisfying TS here
|
|
103
|
-
return theme[color as keyof typeof theme] as RGBA
|
|
104
|
-
}
|
|
105
|
-
return colors()[index % colors().length]
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
const model = iife(() => {
|
|
111
|
-
const [modelStore, setModelStore] = createStore<{
|
|
112
|
-
ready: boolean
|
|
113
|
-
model: Record<
|
|
114
|
-
string,
|
|
115
|
-
| {
|
|
116
|
-
providerID: string
|
|
117
|
-
modelID: string
|
|
118
|
-
}
|
|
119
|
-
| undefined
|
|
120
|
-
>
|
|
121
|
-
override: Record<
|
|
122
|
-
string,
|
|
123
|
-
| {
|
|
124
|
-
providerID: string
|
|
125
|
-
modelID: string
|
|
126
|
-
}
|
|
127
|
-
| undefined
|
|
128
|
-
>
|
|
129
|
-
recent: {
|
|
130
|
-
providerID: string
|
|
131
|
-
modelID: string
|
|
132
|
-
}[]
|
|
133
|
-
favorite: {
|
|
134
|
-
providerID: string
|
|
135
|
-
modelID: string
|
|
136
|
-
}[]
|
|
137
|
-
variant: Record<string, string | undefined>
|
|
138
|
-
}>({
|
|
139
|
-
ready: false,
|
|
140
|
-
model: {},
|
|
141
|
-
override: {},
|
|
142
|
-
recent: [],
|
|
143
|
-
favorite: [],
|
|
144
|
-
variant: {},
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const filePath = path.join(Global.Path.state, "model.json")
|
|
148
|
-
const state = {
|
|
149
|
-
pending: false,
|
|
150
|
-
writer: Promise.resolve() as Promise<unknown>,
|
|
151
|
-
}
|
|
152
|
-
const scope = createMemo(() => project.workspace.current() ?? project.instance.directory())
|
|
153
|
-
|
|
154
|
-
function key(name: string) {
|
|
155
|
-
return [scope(), name].join(":")
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function clear(name: string) {
|
|
159
|
-
setModelStore("model", name, undefined)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function apply(name: string, value: { providerID: string; modelID: string }, persist: boolean) {
|
|
163
|
-
setModelStore("override", key(name), { ...value })
|
|
164
|
-
if (persist) {
|
|
165
|
-
setModelStore("model", name, { ...value })
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
clear(name)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function save() {
|
|
172
|
-
if (!modelStore.ready) {
|
|
173
|
-
state.pending = true
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
state.pending = false
|
|
177
|
-
const data = {
|
|
178
|
-
model: modelStore.model,
|
|
179
|
-
recent: modelStore.recent,
|
|
180
|
-
favorite: modelStore.favorite,
|
|
181
|
-
variant: modelStore.variant,
|
|
182
|
-
}
|
|
183
|
-
state.writer = state.writer.then(() => Filesystem.writeJson(filePath, data)).catch(() => {})
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
Filesystem.readJson(filePath)
|
|
187
|
-
.then((x: any) => {
|
|
188
|
-
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
|
|
189
|
-
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
|
|
190
|
-
if (typeof x.model === "object" && x.model !== null) setModelStore("model", x.model)
|
|
191
|
-
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
|
|
192
|
-
})
|
|
193
|
-
.catch(() => {})
|
|
194
|
-
.finally(() => {
|
|
195
|
-
setModelStore("ready", true)
|
|
196
|
-
if (state.pending) save()
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
const args = useArgs()
|
|
200
|
-
const fallbackModel = createMemo(() => {
|
|
201
|
-
if (args.model) {
|
|
202
|
-
const { providerID, modelID } = parseModel(args.model)
|
|
203
|
-
if (isModelValid({ providerID, modelID })) {
|
|
204
|
-
return {
|
|
205
|
-
providerID,
|
|
206
|
-
modelID,
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (sync.data.config.model) {
|
|
212
|
-
const { providerID, modelID } = parseModel(sync.data.config.model)
|
|
213
|
-
if (isModelValid({ providerID, modelID })) {
|
|
214
|
-
return {
|
|
215
|
-
providerID,
|
|
216
|
-
modelID,
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
for (const item of modelStore.recent) {
|
|
222
|
-
if (isModelValid(item)) {
|
|
223
|
-
return item
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const provider = sync.data.provider[0]
|
|
228
|
-
if (!provider) return undefined
|
|
229
|
-
const defaultModel = sync.data.provider_default[provider.id]
|
|
230
|
-
const firstModel = Object.values(provider.models)[0]
|
|
231
|
-
const model = defaultModel ?? firstModel?.id
|
|
232
|
-
if (!model) return undefined
|
|
233
|
-
return {
|
|
234
|
-
providerID: provider.id,
|
|
235
|
-
modelID: model,
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
const currentModel = createMemo(() => {
|
|
240
|
-
const a = agent.current()
|
|
241
|
-
if (!a) return fallbackModel()
|
|
242
|
-
return (
|
|
243
|
-
getFirstValidModel(
|
|
244
|
-
() => a && modelStore.override[key(a.name)],
|
|
245
|
-
() => a && a.model,
|
|
246
|
-
() => a && modelStore.model[a.name],
|
|
247
|
-
fallbackModel,
|
|
248
|
-
) ?? undefined
|
|
249
|
-
)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
current: currentModel,
|
|
254
|
-
get ready() {
|
|
255
|
-
return modelStore.ready
|
|
256
|
-
},
|
|
257
|
-
saved(name: string) {
|
|
258
|
-
return modelStore.model[name]
|
|
259
|
-
},
|
|
260
|
-
// Used by tests to deterministically await the writer chain instead of sleeping for a fixed
|
|
261
|
-
// duration, which is too slow on Windows CI where temp-file rename can exceed 50ms under AV.
|
|
262
|
-
async flush() {
|
|
263
|
-
const deadline = Date.now() + 5000
|
|
264
|
-
while (state.pending && Date.now() < deadline) await new Promise((r) => setTimeout(r, 0))
|
|
265
|
-
await state.writer
|
|
266
|
-
},
|
|
267
|
-
recent() {
|
|
268
|
-
return modelStore.recent
|
|
269
|
-
},
|
|
270
|
-
favorite() {
|
|
271
|
-
return modelStore.favorite
|
|
272
|
-
},
|
|
273
|
-
parsed: createMemo(() => {
|
|
274
|
-
const value = currentModel()
|
|
275
|
-
if (!value) {
|
|
276
|
-
return {
|
|
277
|
-
provider: "Connect a provider",
|
|
278
|
-
model: "No provider selected",
|
|
279
|
-
reasoning: false,
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const provider = sync.data.provider.find((x) => x.id === value.providerID)
|
|
283
|
-
const info = provider?.models?.[value.modelID]
|
|
284
|
-
return {
|
|
285
|
-
provider: provider?.name ?? value.providerID,
|
|
286
|
-
model: info?.name ?? value.modelID,
|
|
287
|
-
reasoning: info?.capabilities?.reasoning ?? false,
|
|
288
|
-
}
|
|
289
|
-
}),
|
|
290
|
-
cycle(direction: 1 | -1) {
|
|
291
|
-
const current = currentModel()
|
|
292
|
-
if (!current) return
|
|
293
|
-
const recent = modelStore.recent
|
|
294
|
-
const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
|
295
|
-
if (index === -1) return
|
|
296
|
-
let next = index + direction
|
|
297
|
-
if (next < 0) next = recent.length - 1
|
|
298
|
-
if (next >= recent.length) next = 0
|
|
299
|
-
const val = recent[next]
|
|
300
|
-
if (!val) return
|
|
301
|
-
const a = agent.current()
|
|
302
|
-
if (!a) return
|
|
303
|
-
apply(a.name, val, !a.model)
|
|
304
|
-
save()
|
|
305
|
-
},
|
|
306
|
-
cycleFavorite(direction: 1 | -1) {
|
|
307
|
-
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
|
|
308
|
-
if (!favorites.length) {
|
|
309
|
-
toast.show({
|
|
310
|
-
variant: "info",
|
|
311
|
-
message: "Add a favorite model to use this shortcut",
|
|
312
|
-
duration: 3000,
|
|
313
|
-
})
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
const current = currentModel()
|
|
317
|
-
let index = -1
|
|
318
|
-
if (current) {
|
|
319
|
-
index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
|
320
|
-
}
|
|
321
|
-
if (index === -1) {
|
|
322
|
-
index = direction === 1 ? 0 : favorites.length - 1
|
|
323
|
-
} else {
|
|
324
|
-
index += direction
|
|
325
|
-
if (index < 0) index = favorites.length - 1
|
|
326
|
-
if (index >= favorites.length) index = 0
|
|
327
|
-
}
|
|
328
|
-
const next = favorites[index]
|
|
329
|
-
if (!next) return
|
|
330
|
-
const a = agent.current()
|
|
331
|
-
if (!a) return
|
|
332
|
-
apply(a.name, next, !a.model)
|
|
333
|
-
const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
|
|
334
|
-
if (uniq.length > 10) uniq.pop()
|
|
335
|
-
setModelStore(
|
|
336
|
-
"recent",
|
|
337
|
-
uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
|
338
|
-
)
|
|
339
|
-
save()
|
|
340
|
-
},
|
|
341
|
-
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
|
342
|
-
batch(() => {
|
|
343
|
-
if (!isModelValid(model)) {
|
|
344
|
-
toast.show({
|
|
345
|
-
message: `Model ${model.providerID}/${model.modelID} is not valid`,
|
|
346
|
-
variant: "warning",
|
|
347
|
-
duration: 3000,
|
|
348
|
-
})
|
|
349
|
-
return
|
|
350
|
-
}
|
|
351
|
-
const a = agent.current()
|
|
352
|
-
if (!a) return
|
|
353
|
-
apply(a.name, model, !a.model)
|
|
354
|
-
if (options?.recent) {
|
|
355
|
-
const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
|
|
356
|
-
if (uniq.length > 10) uniq.pop()
|
|
357
|
-
setModelStore(
|
|
358
|
-
"recent",
|
|
359
|
-
uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
|
-
save()
|
|
363
|
-
})
|
|
364
|
-
},
|
|
365
|
-
toggleFavorite(model: { providerID: string; modelID: string }) {
|
|
366
|
-
batch(() => {
|
|
367
|
-
if (!isModelValid(model)) {
|
|
368
|
-
toast.show({
|
|
369
|
-
message: `Model ${model.providerID}/${model.modelID} is not valid`,
|
|
370
|
-
variant: "warning",
|
|
371
|
-
duration: 3000,
|
|
372
|
-
})
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
const exists = modelStore.favorite.some(
|
|
376
|
-
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
|
|
377
|
-
)
|
|
378
|
-
const next = exists
|
|
379
|
-
? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
|
380
|
-
: [model, ...modelStore.favorite]
|
|
381
|
-
setModelStore(
|
|
382
|
-
"favorite",
|
|
383
|
-
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
|
|
384
|
-
)
|
|
385
|
-
save()
|
|
386
|
-
})
|
|
387
|
-
},
|
|
388
|
-
variant: {
|
|
389
|
-
selected() {
|
|
390
|
-
const m = currentModel()
|
|
391
|
-
if (!m) return undefined
|
|
392
|
-
const key = `${m.providerID}/${m.modelID}`
|
|
393
|
-
return modelStore.variant[key]
|
|
394
|
-
},
|
|
395
|
-
current() {
|
|
396
|
-
const v = this.selected()
|
|
397
|
-
if (!v) return undefined
|
|
398
|
-
if (!this.list().includes(v)) return undefined
|
|
399
|
-
return v
|
|
400
|
-
},
|
|
401
|
-
list() {
|
|
402
|
-
const m = currentModel()
|
|
403
|
-
if (!m) return []
|
|
404
|
-
const provider = sync.data.provider.find((x) => x.id === m.providerID)
|
|
405
|
-
const info = provider?.models?.[m.modelID]
|
|
406
|
-
if (!info?.variants) return []
|
|
407
|
-
return Object.keys(info.variants)
|
|
408
|
-
},
|
|
409
|
-
set(value: string | undefined) {
|
|
410
|
-
const m = currentModel()
|
|
411
|
-
if (!m) return
|
|
412
|
-
const key = `${m.providerID}/${m.modelID}`
|
|
413
|
-
setModelStore("variant", key, value ?? "default")
|
|
414
|
-
save()
|
|
415
|
-
},
|
|
416
|
-
cycle() {
|
|
417
|
-
const variants = this.list()
|
|
418
|
-
if (variants.length === 0) return
|
|
419
|
-
const current = this.current()
|
|
420
|
-
if (!current) {
|
|
421
|
-
this.set(variants[0])
|
|
422
|
-
return
|
|
423
|
-
}
|
|
424
|
-
const index = variants.indexOf(current)
|
|
425
|
-
if (index === -1 || index === variants.length - 1) {
|
|
426
|
-
this.set(undefined)
|
|
427
|
-
return
|
|
428
|
-
}
|
|
429
|
-
this.set(variants[index + 1])
|
|
430
|
-
},
|
|
431
|
-
},
|
|
432
|
-
}
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
const mcp = {
|
|
436
|
-
isEnabled(name: string) {
|
|
437
|
-
const status = sync.data.mcp[name]
|
|
438
|
-
return status?.status === "connected"
|
|
439
|
-
},
|
|
440
|
-
async toggle(name: string) {
|
|
441
|
-
const status = sync.data.mcp[name]
|
|
442
|
-
if (status?.status === "connected") {
|
|
443
|
-
// Disable: disconnect the MCP
|
|
444
|
-
await sdk.client.mcp.disconnect({ name })
|
|
445
|
-
} else {
|
|
446
|
-
// Enable/Retry: connect the MCP (handles disabled, failed, and other states)
|
|
447
|
-
await sdk.client.mcp.connect({ name })
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
async refresh() {
|
|
451
|
-
const workspace = project.workspace.current()
|
|
452
|
-
await (sdk.client as any).mcp._client.post({
|
|
453
|
-
url: "/mcp/refresh",
|
|
454
|
-
...(workspace ? { workspace } : {}),
|
|
455
|
-
})
|
|
456
|
-
},
|
|
457
|
-
}
|
|
458
|
-
createEffect(() => {
|
|
459
|
-
if (!model.ready) return
|
|
460
|
-
const value = agent.current()
|
|
461
|
-
if (!value) return // guard against empty agent list during org switch
|
|
462
|
-
if (!value.model) return
|
|
463
|
-
if (isModelValid(value.model)) return
|
|
464
|
-
toast.show({
|
|
465
|
-
variant: "warning",
|
|
466
|
-
message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
|
|
467
|
-
duration: 3000,
|
|
468
|
-
})
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
const result = {
|
|
472
|
-
model,
|
|
473
|
-
agent,
|
|
474
|
-
mcp,
|
|
475
|
-
}
|
|
476
|
-
return result
|
|
477
|
-
},
|
|
478
|
-
})
|
|
1
|
+
export * from "./runtime/local"
|
|
@@ -1,41 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type PluginKeybindMap = Record<string, string>
|
|
4
|
-
|
|
5
|
-
type Base = {
|
|
6
|
-
match: (key: string, evt: ParsedKey) => boolean
|
|
7
|
-
print: (key: string) => string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type PluginKeybind = {
|
|
11
|
-
readonly all: PluginKeybindMap
|
|
12
|
-
get: (name: string) => string
|
|
13
|
-
match: (name: string, evt: ParsedKey) => boolean
|
|
14
|
-
print: (name: string) => string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const txt = (value: unknown) => {
|
|
18
|
-
if (typeof value !== "string") return
|
|
19
|
-
if (!value.trim()) return
|
|
20
|
-
return value
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createPluginKeybind(
|
|
24
|
-
base: Base,
|
|
25
|
-
defaults: PluginKeybindMap,
|
|
26
|
-
overrides?: Record<string, unknown>,
|
|
27
|
-
): PluginKeybind {
|
|
28
|
-
const all = Object.freeze(
|
|
29
|
-
Object.fromEntries(Object.entries(defaults).map(([name, value]) => [name, txt(overrides?.[name]) ?? value])),
|
|
30
|
-
)
|
|
31
|
-
const get = (name: string) => all[name] ?? name
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
get all() {
|
|
35
|
-
return all
|
|
36
|
-
},
|
|
37
|
-
get,
|
|
38
|
-
match: (name, evt) => base.match(get(name), evt),
|
|
39
|
-
print: (name) => base.print(get(name)),
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
export * from "./runtime/plugin-keybinds"
|
|
@@ -1,109 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { Path, Workspace } from "@saeeol/sdk/v2"
|
|
3
|
-
import { createStore, reconcile } from "solid-js/store"
|
|
4
|
-
import { createSimpleContext } from "./helper"
|
|
5
|
-
import { useSDK } from "./sdk"
|
|
6
|
-
|
|
7
|
-
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
|
8
|
-
|
|
9
|
-
export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
|
|
10
|
-
name: "Project",
|
|
11
|
-
init: () => {
|
|
12
|
-
const sdk = useSDK()
|
|
13
|
-
|
|
14
|
-
const defaultPath = {
|
|
15
|
-
home: "",
|
|
16
|
-
state: "",
|
|
17
|
-
config: "",
|
|
18
|
-
worktree: "",
|
|
19
|
-
directory: sdk.directory ?? "",
|
|
20
|
-
} satisfies Path
|
|
21
|
-
|
|
22
|
-
const [store, setStore] = createStore({
|
|
23
|
-
project: {
|
|
24
|
-
id: undefined as string | undefined,
|
|
25
|
-
},
|
|
26
|
-
instance: {
|
|
27
|
-
path: defaultPath,
|
|
28
|
-
},
|
|
29
|
-
workspace: {
|
|
30
|
-
current: undefined as string | undefined,
|
|
31
|
-
list: [] as Workspace[],
|
|
32
|
-
status: {} as Record<string, WorkspaceStatus>,
|
|
33
|
-
},
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
async function sync() {
|
|
37
|
-
const workspace = store.workspace.current
|
|
38
|
-
const [path, project] = await Promise.all([
|
|
39
|
-
sdk.client.path.get({ workspace }),
|
|
40
|
-
sdk.client.project.current({ workspace }),
|
|
41
|
-
])
|
|
42
|
-
|
|
43
|
-
batch(() => {
|
|
44
|
-
setStore("instance", "path", reconcile(path.data || defaultPath))
|
|
45
|
-
setStore("project", "id", project.data?.id)
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function syncWorkspace() {
|
|
50
|
-
const listed = await sdk.client.experimental.workspace.list().catch(() => undefined)
|
|
51
|
-
if (!listed?.data) return
|
|
52
|
-
const status = await sdk.client.experimental.workspace.status().catch(() => undefined)
|
|
53
|
-
const next = Object.fromEntries((status?.data ?? []).map((item) => [item.workspaceID, item.status]))
|
|
54
|
-
|
|
55
|
-
batch(() => {
|
|
56
|
-
setStore("workspace", "list", reconcile(listed.data))
|
|
57
|
-
setStore("workspace", "status", reconcile(next))
|
|
58
|
-
if (!listed.data.some((item) => item.id === store.workspace.current)) {
|
|
59
|
-
setStore("workspace", "current", undefined)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
sdk.event.on("event", (event) => {
|
|
65
|
-
if (event.payload.type === "workspace.status") {
|
|
66
|
-
setStore("workspace", "status", event.payload.properties.workspaceID, event.payload.properties.status)
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
data: store,
|
|
72
|
-
project() {
|
|
73
|
-
return store.project.id
|
|
74
|
-
},
|
|
75
|
-
instance: {
|
|
76
|
-
path() {
|
|
77
|
-
return store.instance.path
|
|
78
|
-
},
|
|
79
|
-
directory() {
|
|
80
|
-
return store.instance.path.directory
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
workspace: {
|
|
84
|
-
current() {
|
|
85
|
-
return store.workspace.current
|
|
86
|
-
},
|
|
87
|
-
set(next?: string | null) {
|
|
88
|
-
const workspace = next ?? undefined
|
|
89
|
-
if (store.workspace.current === workspace) return
|
|
90
|
-
setStore("workspace", "current", workspace)
|
|
91
|
-
},
|
|
92
|
-
list() {
|
|
93
|
-
return store.workspace.list
|
|
94
|
-
},
|
|
95
|
-
get(workspaceID: string) {
|
|
96
|
-
return store.workspace.list.find((item) => item.id === workspaceID)
|
|
97
|
-
},
|
|
98
|
-
status(workspaceID: string) {
|
|
99
|
-
return store.workspace.status[workspaceID]
|
|
100
|
-
},
|
|
101
|
-
statuses() {
|
|
102
|
-
return store.workspace.status
|
|
103
|
-
},
|
|
104
|
-
sync: syncWorkspace,
|
|
105
|
-
},
|
|
106
|
-
sync,
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
})
|
|
1
|
+
export * from "./app/project"
|