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
@@ -1,76 +1 @@
1
- import { Global } from "@saeeol/core/global"
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
- import { createStore } from "solid-js/store"
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
- 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
+ export * from "./runtime/plugin-keybinds"
@@ -1,109 +1 @@
1
- import { batch } from "solid-js"
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"