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.
Files changed (52) hide show
  1. package/bin/saeeol.cjs +203 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +2 -2
  4. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +3 -3
  5. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +1 -1
  6. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +5 -5
  7. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +4 -4
  8. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +5 -5
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +3 -3
  10. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +2 -2
  11. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +3 -3
  12. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +4 -4
  13. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +4 -4
  14. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +4 -4
  15. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  16. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  17. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  18. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  19. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  20. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  21. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  22. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  23. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  24. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  25. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  26. package/src/cli/cmd/tui/context/args.tsx +1 -15
  27. package/src/cli/cmd/tui/context/directory.ts +1 -15
  28. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  29. package/src/cli/cmd/tui/context/editor.ts +1 -425
  30. package/src/cli/cmd/tui/context/event.ts +1 -45
  31. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  32. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  33. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  34. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  35. package/src/cli/cmd/tui/context/local.tsx +1 -478
  36. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  37. package/src/cli/cmd/tui/context/project.tsx +1 -109
  38. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  39. package/src/cli/cmd/tui/context/route.tsx +1 -67
  40. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  41. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  42. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  43. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  44. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  45. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  46. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  47. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  48. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  49. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  50. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
  51. package/src/ltm/pipeline.ts +103 -1
  52. 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
- import { createSaeeolClient } from "@saeeol/sdk/v2"
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
+ })