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,25 @@
1
+ import { createContext, Show, useContext, type ParentProps } from "solid-js"
2
+
3
+ export function createSimpleContext<T, Props extends Record<string, any>>(input: {
4
+ name: string
5
+ init: ((input: Props) => T) | (() => T)
6
+ }) {
7
+ const ctx = createContext<T>()
8
+
9
+ return {
10
+ provider: (props: ParentProps<Props>) => {
11
+ const init = input.init(props)
12
+ return (
13
+ // @ts-expect-error
14
+ <Show when={init.ready === undefined || init.ready === true}>
15
+ <ctx.Provider value={init}>{props.children}</ctx.Provider>
16
+ </Show>
17
+ )
18
+ },
19
+ use() {
20
+ const value = useContext(ctx)
21
+ if (!value) throw new Error(`${input.name} context must be used within a context provider`)
22
+ return value
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,109 @@
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
+ })
@@ -0,0 +1,67 @@
1
+ import { createStore, reconcile, unwrap } from "solid-js/store"
2
+ import { createSimpleContext } from "./helper"
3
+ import type { PromptInfo } from "../../component/prompt/history"
4
+
5
+ export type HomeRoute = {
6
+ type: "home"
7
+ prompt?: PromptInfo
8
+ }
9
+
10
+ export type SessionRoute = {
11
+ type: "session"
12
+ sessionID: string
13
+ prompt?: PromptInfo
14
+ }
15
+ export type SaeeolClawRoute = {
16
+ type: "saeeolclaw"
17
+ }
18
+
19
+ export type LocalModelsRoute = {
20
+ type: "local-models"
21
+ }
22
+
23
+ export type PluginRoute = {
24
+ type: "plugin"
25
+ id: string
26
+ data?: Record<string, unknown>
27
+ }
28
+
29
+ export type Route = HomeRoute | SessionRoute | PluginRoute | SaeeolClawRoute | LocalModelsRoute
30
+
31
+ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
32
+ name: "Route",
33
+ init: (props: { initialRoute?: Route }) => {
34
+ const [store, setStore] = createStore<Route>(
35
+ props.initialRoute ??
36
+ (process.env["SAEEOL_ROUTE"]
37
+ ? JSON.parse(process.env["SAEEOL_ROUTE"])
38
+ : {
39
+ type: "home",
40
+ }),
41
+ )
42
+ let previous: Route | undefined
43
+
44
+ return {
45
+ get data() {
46
+ return store
47
+ },
48
+ navigate(route: Route) {
49
+ previous = structuredClone(unwrap(store))
50
+ setStore(reconcile(route))
51
+ },
52
+ back() {
53
+ const target = previous ?? ({ type: "home" } as const)
54
+ previous = undefined
55
+ console.log("navigate", target)
56
+ setStore(target)
57
+ },
58
+ }
59
+ },
60
+ })
61
+
62
+ export type RouteContext = ReturnType<typeof useRoute>
63
+
64
+ export function useRouteData<T extends Route["type"]>(type: T) {
65
+ const route = useRoute()
66
+ return route.data as Extract<Route, { type: typeof type }>
67
+ }
@@ -0,0 +1,142 @@
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
+ })