saeeol 1.2.2 → 1.2.3
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 +187 -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
|
@@ -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
|
+
})
|