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
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { createStore } from "solid-js/store"
|
|
2
|
+
import { createSimpleContext } from "../app/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 "../app/args"
|
|
12
|
+
import { useSDK } from "../app/sdk"
|
|
13
|
+
import { useProject } from "../app/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
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ParsedKey } from "@opentui/core"
|
|
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,142 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { GlobalEvent } from "@saeeol/sdk/v2"
|
|
3
|
-
import { createSimpleContext } from "./helper"
|
|
4
|
-
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
|
5
|
-
import { Flag } from "@saeeol/core/flag/flag"
|
|
6
|
-
import { batch, onCleanup, onMount } from "solid-js"
|
|
7
|
-
|
|
8
|
-
export type EventSource = {
|
|
9
|
-
subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
13
|
-
name: "SDK",
|
|
14
|
-
init: (props: {
|
|
15
|
-
url: string
|
|
16
|
-
directory?: string
|
|
17
|
-
fetch?: typeof fetch
|
|
18
|
-
headers?: RequestInit["headers"]
|
|
19
|
-
events?: EventSource
|
|
20
|
-
}) => {
|
|
21
|
-
const abort = new AbortController()
|
|
22
|
-
let sse: AbortController | undefined
|
|
23
|
-
|
|
24
|
-
function createSDK() {
|
|
25
|
-
return createSaeeolClient({
|
|
26
|
-
baseUrl: props.url,
|
|
27
|
-
signal: abort.signal,
|
|
28
|
-
directory: props.directory,
|
|
29
|
-
fetch: props.fetch,
|
|
30
|
-
headers: props.headers,
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let sdk = createSDK()
|
|
35
|
-
|
|
36
|
-
const emitter = createGlobalEmitter<{
|
|
37
|
-
event: GlobalEvent
|
|
38
|
-
}>()
|
|
39
|
-
|
|
40
|
-
let queue: GlobalEvent[] = []
|
|
41
|
-
let timer: Timer | undefined
|
|
42
|
-
let last = 0
|
|
43
|
-
const retryDelay = 1000
|
|
44
|
-
const maxRetryDelay = 30000
|
|
45
|
-
|
|
46
|
-
const flush = () => {
|
|
47
|
-
if (queue.length === 0) return
|
|
48
|
-
const events = queue
|
|
49
|
-
queue = []
|
|
50
|
-
timer = undefined
|
|
51
|
-
last = Date.now()
|
|
52
|
-
// Batch all event emissions so all store updates result in a single render
|
|
53
|
-
batch(() => {
|
|
54
|
-
for (const event of events) {
|
|
55
|
-
emitter.emit("event", event)
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const handleEvent = (event: GlobalEvent) => {
|
|
61
|
-
queue.push(event)
|
|
62
|
-
const elapsed = Date.now() - last
|
|
63
|
-
|
|
64
|
-
if (timer) return
|
|
65
|
-
// If we just flushed recently (within 16ms), batch this with future events
|
|
66
|
-
// Otherwise, process immediately to avoid latency
|
|
67
|
-
if (elapsed < 16) {
|
|
68
|
-
timer = setTimeout(flush, 16)
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
flush()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function startSSE() {
|
|
75
|
-
sse?.abort()
|
|
76
|
-
const ctrl = new AbortController()
|
|
77
|
-
sse = ctrl
|
|
78
|
-
;(async () => {
|
|
79
|
-
let attempt = 0
|
|
80
|
-
while (true) {
|
|
81
|
-
if (abort.signal.aborted || ctrl.signal.aborted) break
|
|
82
|
-
|
|
83
|
-
const events = await sdk.global.event({
|
|
84
|
-
signal: ctrl.signal,
|
|
85
|
-
sseMaxRetryAttempts: 0,
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
if (Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
|
|
89
|
-
// Start syncing workspaces, it's important to do this after
|
|
90
|
-
// we've started listening to events
|
|
91
|
-
await sdk.sync.start().catch(() => {})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for await (const event of events.stream) {
|
|
95
|
-
if (ctrl.signal.aborted) break
|
|
96
|
-
handleEvent(event)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (timer) clearTimeout(timer)
|
|
100
|
-
if (queue.length > 0) flush()
|
|
101
|
-
attempt += 1
|
|
102
|
-
if (abort.signal.aborted || ctrl.signal.aborted) break
|
|
103
|
-
|
|
104
|
-
// Exponential backoff
|
|
105
|
-
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
|
|
106
|
-
await new Promise((resolve) => setTimeout(resolve, backoff))
|
|
107
|
-
}
|
|
108
|
-
})().catch(() => {})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
onMount(async () => {
|
|
112
|
-
if (props.events) {
|
|
113
|
-
const unsub = await props.events.subscribe(handleEvent)
|
|
114
|
-
onCleanup(unsub)
|
|
115
|
-
|
|
116
|
-
if (Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
|
|
117
|
-
// Start syncing workspaces, it's important to do this after
|
|
118
|
-
// we've started listening to events
|
|
119
|
-
await sdk.sync.start().catch(() => {})
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
startSSE()
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
onCleanup(() => {
|
|
127
|
-
abort.abort()
|
|
128
|
-
sse?.abort()
|
|
129
|
-
if (timer) clearTimeout(timer)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
get client() {
|
|
134
|
-
return sdk
|
|
135
|
-
},
|
|
136
|
-
directory: props.directory,
|
|
137
|
-
event: emitter,
|
|
138
|
-
fetch: props.fetch ?? fetch,
|
|
139
|
-
url: props.url,
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
})
|
|
1
|
+
export * from "./app/sdk"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createSimpleContext } from "../app/helper"
|
|
2
|
+
import type { PromptRef } from "../../component/prompt"
|
|
3
|
+
|
|
4
|
+
export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleContext({
|
|
5
|
+
name: "PromptRef",
|
|
6
|
+
init: () => {
|
|
7
|
+
let current: PromptRef | undefined
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
get current() {
|
|
11
|
+
return current
|
|
12
|
+
},
|
|
13
|
+
set(ref: PromptRef | undefined) {
|
|
14
|
+
current = ref
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
})
|