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
|
@@ -1,18 +1 @@
|
|
|
1
|
-
|
|
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
|
-
})
|
|
1
|
+
export * from "./session/prompt"
|
|
@@ -1,67 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export * from "./app/route"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Event } from "@saeeol/sdk/v2"
|
|
2
|
+
import { useProject } from "../app/project"
|
|
3
|
+
import { useSDK } from "../app/sdk"
|
|
4
|
+
|
|
5
|
+
export function useEvent() {
|
|
6
|
+
const project = useProject()
|
|
7
|
+
const sdk = useSDK()
|
|
8
|
+
|
|
9
|
+
function subscribe(handler: (event: Event) => void) {
|
|
10
|
+
return sdk.event.on("event", (event) => {
|
|
11
|
+
if (event.payload.type === "sync") {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Special hack for truly global events
|
|
16
|
+
if (event.directory === "global") {
|
|
17
|
+
handler(event.payload)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (project.workspace.current()) {
|
|
21
|
+
if (event.workspace === project.workspace.current()) {
|
|
22
|
+
handler(event.payload)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (event.directory === project.instance.directory()) {
|
|
29
|
+
handler(event.payload)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function on<T extends Event["type"]>(type: T, handler: (event: Extract<Event, { type: T }>) => void) {
|
|
35
|
+
return subscribe((event) => {
|
|
36
|
+
if (event.type !== type) return
|
|
37
|
+
handler(event as Extract<Event, { type: T }>)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
subscribe,
|
|
43
|
+
on,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useRenderer } from "@opentui/solid"
|
|
2
|
+
import { createSimpleContext } from "../app/helper"
|
|
3
|
+
import { FormatError, FormatUnknownError } from "@/cli/error"
|
|
4
|
+
import { win32FlushInputBuffer } from "../../win32"
|
|
5
|
+
import { resetTerminalState } from "@/saeeol/cli/cmd/tui/util/terminal"
|
|
6
|
+
type Exit = ((reason?: unknown) => Promise<void>) & {
|
|
7
|
+
message: {
|
|
8
|
+
set: (value?: string) => () => void
|
|
9
|
+
clear: () => void
|
|
10
|
+
get: () => string | undefined
|
|
11
|
+
}
|
|
12
|
+
restart: () => Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
|
16
|
+
name: "Exit",
|
|
17
|
+
init: (input: { onBeforeExit?: () => Promise<void>; onExit?: () => Promise<void> }) => {
|
|
18
|
+
const renderer = useRenderer()
|
|
19
|
+
let message: string | undefined
|
|
20
|
+
let task: Promise<void> | undefined
|
|
21
|
+
const store = {
|
|
22
|
+
set: (value?: string) => {
|
|
23
|
+
const prev = message
|
|
24
|
+
message = value
|
|
25
|
+
return () => {
|
|
26
|
+
message = prev
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
clear: () => {
|
|
30
|
+
message = undefined
|
|
31
|
+
},
|
|
32
|
+
get: () => message,
|
|
33
|
+
}
|
|
34
|
+
const exit: Exit = Object.assign(
|
|
35
|
+
(reason?: unknown) => {
|
|
36
|
+
if (task) return task
|
|
37
|
+
task = (async () => {
|
|
38
|
+
await input.onBeforeExit?.()
|
|
39
|
+
// Reset window title before destroying renderer
|
|
40
|
+
renderer.setTerminalTitle("")
|
|
41
|
+
renderer.destroy()
|
|
42
|
+
win32FlushInputBuffer()
|
|
43
|
+
resetTerminalState()
|
|
44
|
+
if (reason) {
|
|
45
|
+
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
|
|
46
|
+
if (formatted) {
|
|
47
|
+
process.stderr.write(formatted + "\n")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const text = store.get()
|
|
51
|
+
if (text) process.stdout.write(text + "\n")
|
|
52
|
+
await input.onExit?.()
|
|
53
|
+
})()
|
|
54
|
+
return task
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
message: store,
|
|
58
|
+
restart: () => {
|
|
59
|
+
globalThis.__SAEEOL_RESTART = true
|
|
60
|
+
return exit()
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
process.on("SIGHUP", () => exit())
|
|
65
|
+
return exit
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createMemo } from "solid-js"
|
|
2
|
+
import { Keybind } from "@/util/keybind"
|
|
3
|
+
import { pipe, mapValues } from "remeda"
|
|
4
|
+
import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
|
5
|
+
import type { ParsedKey, Renderable } from "@opentui/core"
|
|
6
|
+
import { createStore } from "solid-js/store"
|
|
7
|
+
import { useKeyboard, useRenderer } from "@opentui/solid"
|
|
8
|
+
import { createSimpleContext } from "../app/helper"
|
|
9
|
+
import { useTuiConfig } from "../app/tui-config"
|
|
10
|
+
|
|
11
|
+
export type KeybindKey = keyof NonNullable<TuiConfig.Info["keybinds"]> & string
|
|
12
|
+
|
|
13
|
+
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
|
|
14
|
+
name: "Keybind",
|
|
15
|
+
init: () => {
|
|
16
|
+
const config = useTuiConfig()
|
|
17
|
+
const keybinds = createMemo<Record<string, Keybind.Info[]>>(() => {
|
|
18
|
+
return pipe(
|
|
19
|
+
(config.keybinds ?? {}) as Record<string, string>,
|
|
20
|
+
mapValues((value) => Keybind.parse(value)),
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
const [store, setStore] = createStore({
|
|
24
|
+
leader: false,
|
|
25
|
+
})
|
|
26
|
+
const renderer = useRenderer()
|
|
27
|
+
|
|
28
|
+
let focus: Renderable | null
|
|
29
|
+
let timeout: NodeJS.Timeout
|
|
30
|
+
function leader(active: boolean) {
|
|
31
|
+
if (active) {
|
|
32
|
+
setStore("leader", true)
|
|
33
|
+
focus = renderer.currentFocusedRenderable
|
|
34
|
+
focus?.blur()
|
|
35
|
+
if (timeout) clearTimeout(timeout)
|
|
36
|
+
timeout = setTimeout(() => {
|
|
37
|
+
if (!store.leader) return
|
|
38
|
+
leader(false)
|
|
39
|
+
if (!focus || focus.isDestroyed) return
|
|
40
|
+
focus.focus()
|
|
41
|
+
}, 2000)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!active) {
|
|
46
|
+
if (focus && !renderer.currentFocusedRenderable) {
|
|
47
|
+
focus.focus()
|
|
48
|
+
}
|
|
49
|
+
setStore("leader", false)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
useKeyboard(async (evt) => {
|
|
54
|
+
if (!store.leader && result.match("leader", evt)) {
|
|
55
|
+
leader(true)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (store.leader && evt.name) {
|
|
60
|
+
setImmediate(() => {
|
|
61
|
+
if (focus && renderer.currentFocusedRenderable === focus) {
|
|
62
|
+
focus.focus()
|
|
63
|
+
}
|
|
64
|
+
leader(false)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const result = {
|
|
70
|
+
get all() {
|
|
71
|
+
return keybinds()
|
|
72
|
+
},
|
|
73
|
+
get leader() {
|
|
74
|
+
return store.leader
|
|
75
|
+
},
|
|
76
|
+
parse(evt: ParsedKey): Keybind.Info {
|
|
77
|
+
// Handle special case for Ctrl+Underscore (represented as \x1F)
|
|
78
|
+
if (evt.name === "\x1F") {
|
|
79
|
+
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
|
|
80
|
+
}
|
|
81
|
+
return Keybind.fromParsedKey(evt, store.leader)
|
|
82
|
+
},
|
|
83
|
+
match(key: string, evt: ParsedKey) {
|
|
84
|
+
const list = keybinds()[key] ?? Keybind.parse(key)
|
|
85
|
+
if (!list.length) return false
|
|
86
|
+
const parsed: Keybind.Info = result.parse(evt)
|
|
87
|
+
for (const item of list) {
|
|
88
|
+
if (Keybind.match(item, parsed)) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false
|
|
93
|
+
},
|
|
94
|
+
print(key: string) {
|
|
95
|
+
const first = keybinds()[key]?.at(0) ?? Keybind.parse(key).at(0)
|
|
96
|
+
if (!first) return ""
|
|
97
|
+
const text = Keybind.toString(first)
|
|
98
|
+
const lead = keybinds().leader?.[0]
|
|
99
|
+
if (!lead) return text
|
|
100
|
+
return text.replace("<leader>", Keybind.toString(lead))
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
return result
|
|
104
|
+
},
|
|
105
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
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 "../app/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
|
+
})
|