saeeol 1.2.0 → 1.2.2

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 (193) hide show
  1. package/package.json +14 -14
  2. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  3. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  4. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  20. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  21. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  22. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  23. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  24. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  25. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  26. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  27. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  28. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  29. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  30. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  31. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  32. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  33. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  34. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  35. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  36. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  37. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  38. package/src/session/compaction-helpers.ts +1 -169
  39. package/src/session/compaction.ts +1 -712
  40. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  41. package/src/session/core/compaction/compaction.ts +712 -0
  42. package/src/session/core/compaction/overflow.ts +28 -0
  43. package/src/session/core/instruction.ts +234 -0
  44. package/src/session/core/llm.ts +504 -0
  45. package/src/session/core/network.ts +392 -0
  46. package/src/session/core/processor.ts +731 -0
  47. package/src/session/core/projectors.ts +139 -0
  48. package/src/session/core/resolve-tools.ts +241 -0
  49. package/src/session/core/retry.ts +149 -0
  50. package/src/session/core/revert.ts +173 -0
  51. package/src/session/core/run-state.ts +110 -0
  52. package/src/session/core/schema.ts +35 -0
  53. package/src/session/core/session-types.ts +160 -0
  54. package/src/session/core/session.sql.ts +124 -0
  55. package/src/session/core/session.ts +948 -0
  56. package/src/session/core/shell-exec.ts +205 -0
  57. package/src/session/core/status.ts +100 -0
  58. package/src/session/core/subtask.ts +268 -0
  59. package/src/session/core/summary.ts +173 -0
  60. package/src/session/core/system.ts +114 -0
  61. package/src/session/core/todo.ts +86 -0
  62. package/src/session/core/user-part.ts +293 -0
  63. package/src/session/instruction.ts +1 -234
  64. package/src/session/llm.ts +1 -504
  65. package/src/session/message/message-errors.ts +83 -0
  66. package/src/session/message/message-parts.ts +89 -0
  67. package/src/session/message/message-query.ts +107 -0
  68. package/src/session/message/message-transform.ts +156 -0
  69. package/src/session/message/message-types.ts +68 -0
  70. package/src/session/message/message-v2.ts +73 -0
  71. package/src/session/message/message.ts +192 -0
  72. package/src/session/message-errors.ts +1 -83
  73. package/src/session/message-parts.ts +1 -89
  74. package/src/session/message-query.ts +1 -107
  75. package/src/session/message-transform.ts +1 -156
  76. package/src/session/message-types.ts +1 -68
  77. package/src/session/message-v2.ts +1 -73
  78. package/src/session/message.ts +1 -192
  79. package/src/session/network.ts +1 -392
  80. package/src/session/overflow.ts +1 -28
  81. package/src/session/processor.ts +1 -731
  82. package/src/session/projectors.ts +2 -139
  83. package/src/session/prompt/prompt-command.ts +93 -0
  84. package/src/session/prompt/prompt-loop.ts +299 -0
  85. package/src/session/prompt/prompt-model.ts +44 -0
  86. package/src/session/prompt/prompt-reminders.ts +120 -0
  87. package/src/session/prompt/prompt-resolve.ts +42 -0
  88. package/src/session/prompt/prompt-schemas.ts +128 -0
  89. package/src/session/prompt/prompt-title.ts +55 -0
  90. package/src/session/prompt/prompt-types.ts +47 -0
  91. package/src/session/prompt/prompt-user-msg.ts +80 -0
  92. package/src/session/prompt/prompt.ts +211 -0
  93. package/src/session/prompt-command.ts +1 -93
  94. package/src/session/prompt-loop.ts +1 -299
  95. package/src/session/prompt-model.ts +1 -44
  96. package/src/session/prompt-reminders.ts +1 -120
  97. package/src/session/prompt-resolve.ts +1 -42
  98. package/src/session/prompt-schemas.ts +1 -128
  99. package/src/session/prompt-title.ts +1 -55
  100. package/src/session/prompt-types.ts +1 -47
  101. package/src/session/prompt-user-msg.ts +1 -80
  102. package/src/session/prompt.ts +1 -211
  103. package/src/session/resolve-tools.ts +1 -241
  104. package/src/session/retry.ts +1 -149
  105. package/src/session/revert.ts +1 -173
  106. package/src/session/run-state.ts +1 -110
  107. package/src/session/schema.ts +1 -35
  108. package/src/session/session-types.ts +1 -160
  109. package/src/session/session.sql.ts +1 -124
  110. package/src/session/session.ts +1 -948
  111. package/src/session/shell-exec.ts +1 -205
  112. package/src/session/status.ts +1 -100
  113. package/src/session/subtask.ts +1 -268
  114. package/src/session/summary.ts +1 -173
  115. package/src/session/system.ts +1 -114
  116. package/src/session/todo.ts +1 -86
  117. package/src/session/user-part.ts +1 -293
  118. package/src/tool/apply_patch.ts +1 -334
  119. package/src/tool/bash.ts +1 -656
  120. package/src/tool/core/external-directory.ts +55 -0
  121. package/src/tool/core/invalid.ts +21 -0
  122. package/src/tool/core/recall.ts +164 -0
  123. package/src/tool/core/recall.txt +12 -0
  124. package/src/tool/core/schema.ts +16 -0
  125. package/src/tool/core/tool.ts +162 -0
  126. package/src/tool/core/truncate.ts +160 -0
  127. package/src/tool/core/truncation-dir.ts +4 -0
  128. package/src/tool/diagnostics.ts +1 -20
  129. package/src/tool/edit-replacers.ts +1 -288
  130. package/src/tool/edit-utils.ts +1 -86
  131. package/src/tool/edit.ts +1 -262
  132. package/src/tool/external-directory.ts +1 -55
  133. package/src/tool/file/apply_patch.ts +334 -0
  134. package/src/tool/file/apply_patch.txt +33 -0
  135. package/src/tool/file/bash.ts +656 -0
  136. package/src/tool/file/bash.txt +119 -0
  137. package/src/tool/file/edit-replacers.ts +288 -0
  138. package/src/tool/file/edit-utils.ts +86 -0
  139. package/src/tool/file/edit.ts +262 -0
  140. package/src/tool/file/edit.txt +10 -0
  141. package/src/tool/file/read.ts +389 -0
  142. package/src/tool/file/read.txt +14 -0
  143. package/src/tool/file/write.ts +114 -0
  144. package/src/tool/file/write.txt +8 -0
  145. package/src/tool/glob.ts +1 -115
  146. package/src/tool/grep.ts +1 -151
  147. package/src/tool/integration/diagnostics.ts +20 -0
  148. package/src/tool/integration/lsp.ts +113 -0
  149. package/src/tool/integration/lsp.txt +24 -0
  150. package/src/tool/integration/mcp-exa.ts +73 -0
  151. package/src/tool/integration/package.ts +168 -0
  152. package/src/tool/integration/registry.ts +375 -0
  153. package/src/tool/invalid.ts +1 -21
  154. package/src/tool/lsp.ts +1 -113
  155. package/src/tool/mcp-exa.ts +1 -73
  156. package/src/tool/package.ts +1 -168
  157. package/src/tool/plan.ts +1 -30
  158. package/src/tool/question.ts +1 -52
  159. package/src/tool/read.ts +1 -389
  160. package/src/tool/recall.ts +1 -164
  161. package/src/tool/registry.ts +1 -375
  162. package/src/tool/schema.ts +1 -16
  163. package/src/tool/search/glob.ts +115 -0
  164. package/src/tool/search/glob.txt +6 -0
  165. package/src/tool/search/grep.ts +151 -0
  166. package/src/tool/search/grep.txt +8 -0
  167. package/src/tool/search/warpgrep.ts +107 -0
  168. package/src/tool/search/warpgrep.txt +10 -0
  169. package/src/tool/search/webfetch.ts +202 -0
  170. package/src/tool/search/webfetch.txt +13 -0
  171. package/src/tool/search/websearch.ts +71 -0
  172. package/src/tool/search/websearch.txt +14 -0
  173. package/src/tool/skill.ts +1 -91
  174. package/src/tool/task.ts +1 -197
  175. package/src/tool/todo.ts +1 -62
  176. package/src/tool/tool.ts +1 -162
  177. package/src/tool/truncate.ts +1 -160
  178. package/src/tool/truncation-dir.ts +1 -4
  179. package/src/tool/warpgrep.ts +1 -107
  180. package/src/tool/webfetch.ts +1 -202
  181. package/src/tool/websearch.ts +1 -71
  182. package/src/tool/workflow/plan-enter.txt +14 -0
  183. package/src/tool/workflow/plan-exit.txt +13 -0
  184. package/src/tool/workflow/plan.ts +30 -0
  185. package/src/tool/workflow/question.ts +52 -0
  186. package/src/tool/workflow/question.txt +11 -0
  187. package/src/tool/workflow/skill.ts +91 -0
  188. package/src/tool/workflow/skill.txt +5 -0
  189. package/src/tool/workflow/task.ts +197 -0
  190. package/src/tool/workflow/task.txt +57 -0
  191. package/src/tool/workflow/todo.ts +62 -0
  192. package/src/tool/workflow/todowrite.txt +167 -0
  193. package/src/tool/write.ts +1 -114
@@ -0,0 +1,50 @@
1
+ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
2
+ import { useTheme } from "../context/theme"
3
+ import { useDialog } from "../ui/dialog"
4
+ import { onCleanup } from "solid-js"
5
+
6
+ export function DialogThemeList() {
7
+ const theme = useTheme()
8
+ const options = Object.keys(theme.all())
9
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }))
10
+ .map((value) => ({
11
+ title: value,
12
+ value: value,
13
+ }))
14
+ const dialog = useDialog()
15
+ let confirmed = false
16
+ let ref: DialogSelectRef<string>
17
+ const initial = theme.selected
18
+
19
+ onCleanup(() => {
20
+ if (!confirmed) theme.set(initial)
21
+ })
22
+
23
+ return (
24
+ <DialogSelect
25
+ title="Themes"
26
+ options={options}
27
+ current={initial}
28
+ onMove={(opt) => {
29
+ theme.set(opt.value)
30
+ }}
31
+ onSelect={(opt) => {
32
+ theme.set(opt.value)
33
+ confirmed = true
34
+ dialog.clear()
35
+ }}
36
+ ref={(r) => {
37
+ ref = r
38
+ }}
39
+ onFilter={(query) => {
40
+ if (query.length === 0) {
41
+ theme.set(initial)
42
+ return
43
+ }
44
+
45
+ const first = ref.filtered[0]
46
+ if (first) theme.set(first.value)
47
+ }}
48
+ />
49
+ )
50
+ }
@@ -0,0 +1,39 @@
1
+ import { createMemo } from "solid-js"
2
+ import { useLocal } from "@tui/context/local"
3
+ import { DialogSelect } from "@tui/ui/dialog-select"
4
+ import { useDialog } from "@tui/ui/dialog"
5
+
6
+ export function DialogVariant() {
7
+ const local = useLocal()
8
+ const dialog = useDialog()
9
+
10
+ const options = createMemo(() => {
11
+ return [
12
+ {
13
+ value: "default",
14
+ title: "Default",
15
+ onSelect: () => {
16
+ dialog.clear()
17
+ local.model.variant.set(undefined)
18
+ },
19
+ },
20
+ ...local.model.variant.list().map((variant) => ({
21
+ value: variant,
22
+ title: variant,
23
+ onSelect: () => {
24
+ dialog.clear()
25
+ local.model.variant.set(variant)
26
+ },
27
+ })),
28
+ ]
29
+ })
30
+
31
+ return (
32
+ <DialogSelect<string>
33
+ options={options()}
34
+ title={"Select variant"}
35
+ current={local.model.variant.selected()}
36
+ flat={true}
37
+ />
38
+ )
39
+ }
@@ -0,0 +1,200 @@
1
+ import { createSaeeolClient } from "@saeeol/sdk/v2"
2
+ import { useDialog } from "@tui/ui/dialog"
3
+ import { DialogSelect } from "@tui/ui/dialog-select"
4
+ import { useRoute } from "@tui/context/route"
5
+ import { useSync } from "@tui/context/sync"
6
+ import { useProject } from "@tui/context/project"
7
+ import { createMemo, createSignal, onMount } from "solid-js"
8
+ import { setTimeout as sleep } from "node:timers/promises"
9
+ import { errorMessage } from "@/util/error"
10
+ import { useSDK } from "../context/sdk"
11
+ import { useToast } from "../ui/toast"
12
+
13
+ type Adapter = {
14
+ type: string
15
+ name: string
16
+ description: string
17
+ }
18
+
19
+ function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
20
+ return createSaeeolClient({
21
+ baseUrl: sdk.url,
22
+ fetch: sdk.fetch,
23
+ directory: sync.path.directory || sdk.directory,
24
+ experimental_workspaceID: workspaceID,
25
+ })
26
+ }
27
+
28
+ export async function openWorkspaceSession(input: {
29
+ dialog: ReturnType<typeof useDialog>
30
+ route: ReturnType<typeof useRoute>
31
+ sdk: ReturnType<typeof useSDK>
32
+ sync: ReturnType<typeof useSync>
33
+ toast: ReturnType<typeof useToast>
34
+ workspaceID: string
35
+ }) {
36
+ const client = scoped(input.sdk, input.sync, input.workspaceID)
37
+
38
+ while (true) {
39
+ const result = await client.session.create({ workspace: input.workspaceID }).catch(() => undefined)
40
+ if (!result) {
41
+ input.toast.show({
42
+ message: "Failed to create workspace session",
43
+ variant: "error",
44
+ })
45
+ return
46
+ }
47
+ if (result.response?.status && result.response.status >= 500 && result.response.status < 600) {
48
+ await sleep(1000)
49
+ continue
50
+ }
51
+ if (!result.data) {
52
+ input.toast.show({
53
+ message: "Failed to create workspace session",
54
+ variant: "error",
55
+ })
56
+ return
57
+ }
58
+
59
+ input.route.navigate({
60
+ type: "session",
61
+ sessionID: result.data.id,
62
+ })
63
+ input.dialog.clear()
64
+ return
65
+ }
66
+ }
67
+
68
+ export async function restoreWorkspaceSession(input: {
69
+ dialog: ReturnType<typeof useDialog>
70
+ sdk: ReturnType<typeof useSDK>
71
+ sync: ReturnType<typeof useSync>
72
+ project: ReturnType<typeof useProject>
73
+ toast: ReturnType<typeof useToast>
74
+ workspaceID: string
75
+ sessionID: string
76
+ done?: () => void
77
+ }) {
78
+ const result = await input.sdk.client.experimental.workspace
79
+ .sessionRestore({ id: input.workspaceID, sessionID: input.sessionID })
80
+ .catch(() => undefined)
81
+ if (!result?.data) {
82
+ input.toast.show({
83
+ message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`,
84
+ variant: "error",
85
+ })
86
+ return
87
+ }
88
+
89
+ input.project.workspace.set(input.workspaceID)
90
+
91
+ await input.sync.bootstrap({ fatal: false }).catch(() => undefined)
92
+
93
+ await Promise.all([input.project.workspace.sync(), input.sync.session.sync(input.sessionID)])
94
+
95
+ input.toast.show({
96
+ message: "Session restored into the new workspace",
97
+ variant: "success",
98
+ })
99
+ input.done?.()
100
+ if (input.done) return
101
+ input.dialog.clear()
102
+ }
103
+
104
+ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
105
+ const dialog = useDialog()
106
+ const sync = useSync()
107
+ const project = useProject()
108
+ const sdk = useSDK()
109
+ const toast = useToast()
110
+ const [creating, setCreating] = createSignal<string>()
111
+ const [adapters, setAdapters] = createSignal<Adapter[]>()
112
+
113
+ onMount(() => {
114
+ dialog.setSize("medium")
115
+ void (async () => {
116
+ const dir = sync.path.directory || sdk.directory
117
+ const url = new URL("/experimental/workspace/adapter", sdk.url)
118
+ if (dir) url.searchParams.set("directory", dir)
119
+ const res = await sdk
120
+ .fetch(url)
121
+ .then((x) => x.json() as Promise<Adapter[]>)
122
+ .catch(() => undefined)
123
+ if (!res) {
124
+ toast.show({
125
+ message: "Failed to load workspace adapters",
126
+ variant: "error",
127
+ })
128
+ return
129
+ }
130
+ setAdapters(res)
131
+ })()
132
+ })
133
+
134
+ const options = createMemo(() => {
135
+ const type = creating()
136
+ if (type) {
137
+ return [
138
+ {
139
+ title: `Creating ${type} workspace...`,
140
+ value: "creating" as const,
141
+ description: "This can take a while for remote environments",
142
+ },
143
+ ]
144
+ }
145
+ const list = adapters()
146
+ if (!list) {
147
+ return [
148
+ {
149
+ title: "Loading workspaces...",
150
+ value: "loading" as const,
151
+ description: "Fetching available workspace adapters",
152
+ },
153
+ ]
154
+ }
155
+ return list.map((item) => ({
156
+ title: item.name,
157
+ value: item.type,
158
+ description: item.description,
159
+ }))
160
+ })
161
+
162
+ const create = async (type: string) => {
163
+ if (creating()) return
164
+ setCreating(type)
165
+
166
+ const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => {
167
+ toast.show({
168
+ message: "Creating workspace failed",
169
+ variant: "error",
170
+ })
171
+ return undefined
172
+ })
173
+
174
+ const workspace = result?.data
175
+ if (!workspace) {
176
+ setCreating(undefined)
177
+ toast.show({
178
+ message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
179
+ variant: "error",
180
+ })
181
+ return
182
+ }
183
+
184
+ await project.workspace.sync()
185
+ await props.onSelect(workspace.id)
186
+ setCreating(undefined)
187
+ }
188
+
189
+ return (
190
+ <DialogSelect
191
+ title={creating() ? "Creating Workspace" : "New Workspace"}
192
+ skipFilter={true}
193
+ options={options()}
194
+ onSelect={(option) => {
195
+ if (option.value === "creating" || option.value === "loading") return
196
+ void create(option.value)
197
+ }}
198
+ />
199
+ )
200
+ }
@@ -0,0 +1,81 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { createStore } from "solid-js/store"
4
+ import { For } from "solid-js"
5
+ import { useTheme } from "../context/theme"
6
+ import { useDialog } from "../ui/dialog"
7
+
8
+ export function DialogWorkspaceUnavailable(props: { onRestore?: () => boolean | void | Promise<boolean | void> }) {
9
+ const dialog = useDialog()
10
+ const { theme } = useTheme()
11
+ const [store, setStore] = createStore({
12
+ active: "restore" as "cancel" | "restore",
13
+ })
14
+
15
+ const options = ["cancel", "restore"] as const
16
+
17
+ async function confirm() {
18
+ if (store.active === "cancel") {
19
+ dialog.clear()
20
+ return
21
+ }
22
+ const result = await props.onRestore?.()
23
+ if (result === false) return
24
+ }
25
+
26
+ useKeyboard((evt) => {
27
+ if (evt.name === "return") {
28
+ evt.preventDefault()
29
+ evt.stopPropagation()
30
+ void confirm()
31
+ return
32
+ }
33
+ if (evt.name === "left") {
34
+ evt.preventDefault()
35
+ evt.stopPropagation()
36
+ setStore("active", "cancel")
37
+ return
38
+ }
39
+ if (evt.name === "right") {
40
+ evt.preventDefault()
41
+ evt.stopPropagation()
42
+ setStore("active", "restore")
43
+ }
44
+ })
45
+
46
+ return (
47
+ <box paddingLeft={2} paddingRight={2} gap={1}>
48
+ <box flexDirection="row" justifyContent="space-between">
49
+ <text attributes={TextAttributes.BOLD} fg={theme.text}>
50
+ Workspace Unavailable
51
+ </text>
52
+ <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
53
+ esc
54
+ </text>
55
+ </box>
56
+ <text fg={theme.textMuted} wrapMode="word">
57
+ This session is attached to a workspace that is no longer available.
58
+ </text>
59
+ <text fg={theme.textMuted} wrapMode="word">
60
+ Would you like to restore this session into a new workspace?
61
+ </text>
62
+ <box flexDirection="row" justifyContent="flex-end" paddingBottom={1} gap={1}>
63
+ <For each={options}>
64
+ {(item) => (
65
+ <box
66
+ paddingLeft={2}
67
+ paddingRight={2}
68
+ backgroundColor={item === store.active ? theme.primary : undefined}
69
+ onMouseUp={() => {
70
+ setStore("active", item)
71
+ void confirm()
72
+ }}
73
+ >
74
+ <text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
75
+ </box>
76
+ )}
77
+ </For>
78
+ </box>
79
+ </box>
80
+ )
81
+ }
@@ -1,32 +1 @@
1
- import { createMemo } from "solid-js"
2
- import { useLocal } from "@tui/context/local"
3
- import { DialogSelect } from "@tui/ui/dialog-select"
4
- import { useDialog } from "@tui/ui/dialog"
5
-
6
- export function DialogAgent() {
7
- const local = useLocal()
8
- const dialog = useDialog()
9
-
10
- const options = createMemo(() =>
11
- local.agent.list().map((item) => {
12
- return {
13
- value: item.name,
14
- title: item.displayName ?? item.name,
15
- description:
16
- [item.deprecated && "deprecated", item.native && "native"].filter(Boolean).join(", ") || item.description,
17
- }
18
- }),
19
- )
20
-
21
- return (
22
- <DialogSelect
23
- title="Select agent"
24
- current={local.agent.current()?.name ?? ""}
25
- options={options()}
26
- onSelect={(option) => {
27
- local.agent.set(option.value)
28
- dialog.clear()
29
- }}
30
- />
31
- )
32
- }
1
+ export * from "./dialog/dialog-agent"
@@ -1,190 +1 @@
1
- import { useDialog } from "@tui/ui/dialog"
2
- import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
3
- import {
4
- createContext,
5
- createMemo,
6
- createSignal,
7
- getOwner,
8
- onCleanup,
9
- runWithOwner,
10
- useContext,
11
- type Accessor,
12
- type ParentProps,
13
- } from "solid-js"
14
- import { useKeyboard } from "@opentui/solid"
15
- import { useKeybind } from "@tui/context/keybind"
16
- import { t } from "@/util/i18n"
17
-
18
- type Context = ReturnType<typeof init>
19
- const ctx = createContext<Context>()
20
-
21
- export type Slash = {
22
- name: string
23
- aliases?: string[]
24
- }
25
-
26
- export type CommandOption = DialogSelectOption<string> & {
27
- keybind?: string
28
- suggested?: boolean
29
- slash?: Slash
30
- hidden?: boolean
31
- enabled?: boolean
32
- }
33
-
34
- function init() {
35
- const root = getOwner()
36
- const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
37
- const [suspendCount, setSuspendCount] = createSignal(0)
38
- const dialog = useDialog()
39
- const keybind = useKeybind()
40
-
41
- // Double-tap tracking for agent_cycle keybinds
42
- const DOUBLE_TAB_KEYS = new Set(["agent_cycle", "agent_cycle_reverse"])
43
- const DOUBLE_TAB_WINDOW = 400
44
- let lastTabKey = ""
45
- let lastTabTime = 0
46
-
47
- const entries = createMemo(() => {
48
- const all = registrations().flatMap((x) => x())
49
- return all.map((x) => ({
50
- ...x,
51
- footer: x.keybind ? keybind.print(x.keybind) : undefined,
52
- }))
53
- })
54
-
55
- const isEnabled = (option: CommandOption) => option.enabled !== false
56
- const isVisible = (option: CommandOption) => isEnabled(option) && !option.hidden
57
-
58
- const visibleOptions = createMemo(() => entries().filter((option) => isVisible(option)))
59
- const suggestedOptions = createMemo(() =>
60
- visibleOptions()
61
- .filter((option) => option.suggested)
62
- .map((option) => ({
63
- ...option,
64
- value: `suggested:${option.value}`,
65
- category: t("cmd.suggested"),
66
- })),
67
- )
68
- const suspended = () => suspendCount() > 0
69
-
70
- useKeyboard((evt) => {
71
- if (suspended()) return
72
- if (dialog.stack.length > 0) return
73
- if (evt.defaultPrevented) return
74
- for (const option of entries()) {
75
- if (!isEnabled(option)) continue
76
- if (option.keybind && keybind.match(option.keybind, evt)) {
77
- // Require double-tap for agent cycle keybinds
78
- if (DOUBLE_TAB_KEYS.has(option.keybind)) {
79
- const now = Date.now()
80
- const match = option.keybind === lastTabKey && now - lastTabTime < DOUBLE_TAB_WINDOW
81
- lastTabKey = option.keybind
82
- lastTabTime = now
83
- if (!match) {
84
- evt.preventDefault()
85
- return
86
- }
87
- }
88
- evt.preventDefault()
89
- option.onSelect?.(dialog)
90
- return
91
- }
92
- }
93
- })
94
-
95
- const result = {
96
- trigger(name: string) {
97
- for (const option of entries()) {
98
- if (option.value === name) {
99
- if (!isEnabled(option)) return
100
- option.onSelect?.(dialog)
101
- return
102
- }
103
- }
104
- },
105
- slashes() {
106
- return visibleOptions().flatMap((option) => {
107
- const slash = option.slash
108
- if (!slash) return []
109
- return {
110
- display: "/" + slash.name,
111
- description: option.description ?? option.title,
112
- aliases: slash.aliases?.map((alias) => "/" + alias),
113
- onSelect: () => result.trigger(option.value),
114
- }
115
- })
116
- },
117
- keybinds(enabled: boolean) {
118
- setSuspendCount((count) => count + (enabled ? -1 : 1))
119
- },
120
- suspended,
121
- show() {
122
- dialog.replace(() => <DialogCommand options={visibleOptions()} suggestedOptions={suggestedOptions()} />)
123
- },
124
- register(cb: () => CommandOption[]) {
125
- const owner = getOwner() ?? root
126
- if (!owner) return () => {}
127
-
128
- let list: Accessor<CommandOption[]> | undefined
129
-
130
- // TUI plugins now register commands via an async store that runs outside an active reactive scope.
131
- // runWithOwner attaches createMemo/onCleanup to this owner so plugin registrations stay reactive and dispose correctly.
132
- runWithOwner(owner, () => {
133
- list = createMemo(cb)
134
- const ref = list
135
- if (!ref) return
136
- setRegistrations((arr) => [ref, ...arr])
137
- onCleanup(() => {
138
- setRegistrations((arr) => arr.filter((x) => x !== ref))
139
- })
140
- })
141
-
142
- if (!list) return () => {}
143
- let done = false
144
- return () => {
145
- if (done) return
146
- done = true
147
- const ref = list
148
- if (!ref) return
149
- setRegistrations((arr) => arr.filter((x) => x !== ref))
150
- }
151
- },
152
- }
153
- return result
154
- }
155
-
156
- export function useCommandDialog() {
157
- const value = useContext(ctx)
158
- if (!value) {
159
- throw new Error("useCommandDialog must be used within a CommandProvider")
160
- }
161
- return value
162
- }
163
-
164
- export function CommandProvider(props: ParentProps) {
165
- const value = init()
166
- const dialog = useDialog()
167
- const keybind = useKeybind()
168
-
169
- useKeyboard((evt) => {
170
- if (value.suspended()) return
171
- if (dialog.stack.length > 0) return
172
- if (evt.defaultPrevented) return
173
- if (keybind.match("command_list", evt)) {
174
- evt.preventDefault()
175
- value.show()
176
- return
177
- }
178
- })
179
-
180
- return <ctx.Provider value={value}>{props.children}</ctx.Provider>
181
- }
182
-
183
- function DialogCommand(props: { options: CommandOption[]; suggestedOptions: CommandOption[] }) {
184
- let ref: DialogSelectRef<string>
185
- const list = () => {
186
- if (ref?.filter) return props.options
187
- return [...props.suggestedOptions, ...props.options]
188
- }
189
- return <DialogSelect ref={(r) => (ref = r)} title={t("cmd.title")} options={list()} />
190
- }
1
+ export * from "./dialog/dialog-command"
@@ -1,103 +1 @@
1
- import { createResource, createMemo } from "solid-js"
2
- import { DialogSelect } from "@tui/ui/dialog-select"
3
- import { useSDK } from "@tui/context/sdk"
4
- import { useDialog } from "@tui/ui/dialog"
5
- import { useToast } from "@tui/ui/toast"
6
- import { useTheme } from "@tui/context/theme"
7
- import type { ExperimentalConsoleListOrgsResponse } from "@saeeol/sdk/v2"
8
-
9
- type OrgOption = ExperimentalConsoleListOrgsResponse["orgs"][number]
10
-
11
- const accountHost = (url: string) => {
12
- try {
13
- return new URL(url).host
14
- } catch {
15
- return url
16
- }
17
- }
18
-
19
- const accountLabel = (item: Pick<OrgOption, "accountEmail" | "accountUrl">) =>
20
- `${item.accountEmail} ${accountHost(item.accountUrl)}`
21
-
22
- export function DialogConsoleOrg() {
23
- const sdk = useSDK()
24
- const dialog = useDialog()
25
- const toast = useToast()
26
- const { theme } = useTheme()
27
-
28
- const [orgs] = createResource(async () => {
29
- const result = await sdk.client.experimental.console.listOrgs({}, { throwOnError: true })
30
- return result.data?.orgs ?? []
31
- })
32
-
33
- const current = createMemo(() => orgs()?.find((item) => item.active))
34
-
35
- const options = createMemo(() => {
36
- const listed = orgs()
37
- if (listed === undefined) {
38
- return [
39
- {
40
- title: "Loading orgs...",
41
- value: "loading",
42
- onSelect: () => {},
43
- },
44
- ]
45
- }
46
-
47
- if (listed.length === 0) {
48
- return [
49
- {
50
- title: "No orgs found",
51
- value: "empty",
52
- onSelect: () => {},
53
- },
54
- ]
55
- }
56
-
57
- return listed
58
- .toSorted((a, b) => {
59
- const activeAccountA = a.active ? 0 : 1
60
- const activeAccountB = b.active ? 0 : 1
61
- if (activeAccountA !== activeAccountB) return activeAccountA - activeAccountB
62
-
63
- const accountCompare = accountLabel(a).localeCompare(accountLabel(b))
64
- if (accountCompare !== 0) return accountCompare
65
-
66
- return a.orgName.localeCompare(b.orgName)
67
- })
68
- .map((item) => ({
69
- title: item.orgName,
70
- value: item,
71
- category: accountLabel(item),
72
- categoryView: (
73
- <box flexDirection="row" gap={2}>
74
- <text fg={theme.accent}>{item.accountEmail}</text>
75
- <text fg={theme.textMuted}>{accountHost(item.accountUrl)}</text>
76
- </box>
77
- ),
78
- onSelect: async () => {
79
- if (item.active) {
80
- dialog.clear()
81
- return
82
- }
83
-
84
- await sdk.client.experimental.console.switchOrg(
85
- {
86
- accountID: item.accountID,
87
- orgID: item.orgID,
88
- },
89
- { throwOnError: true },
90
- )
91
-
92
- await sdk.client.instance.dispose()
93
- toast.show({
94
- message: `Switched to ${item.orgName}`,
95
- variant: "info",
96
- })
97
- dialog.clear()
98
- },
99
- }))
100
- })
101
-
102
- return <DialogSelect<string | OrgOption> title="Switch org" options={options()} current={current()} />
103
- }
1
+ export * from "./dialog/dialog-console-org"