saeeol 1.2.1 → 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.
Files changed (151) hide show
  1. package/bin/saeeol.cjs +187 -0
  2. package/npm/bin/saeeol +0 -0
  3. package/package.json +12 -12
  4. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  20. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  21. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  22. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  23. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  24. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  25. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  26. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  27. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  28. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  29. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  30. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  31. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  32. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  33. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  34. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  35. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  36. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  37. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  38. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  39. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  40. package/src/cli/cmd/tui/context/app/args.tsx +15 -0
  41. package/src/cli/cmd/tui/context/app/directory.ts +15 -0
  42. package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
  43. package/src/cli/cmd/tui/context/app/editor.ts +425 -0
  44. package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
  45. package/src/cli/cmd/tui/context/app/project.tsx +109 -0
  46. package/src/cli/cmd/tui/context/app/route.tsx +67 -0
  47. package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
  48. package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
  49. package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
  50. package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
  51. package/src/cli/cmd/tui/context/args.tsx +1 -15
  52. package/src/cli/cmd/tui/context/directory.ts +1 -15
  53. package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
  54. package/src/cli/cmd/tui/context/editor.ts +1 -425
  55. package/src/cli/cmd/tui/context/event.ts +1 -45
  56. package/src/cli/cmd/tui/context/exit.tsx +1 -67
  57. package/src/cli/cmd/tui/context/helper.tsx +1 -25
  58. package/src/cli/cmd/tui/context/keybind.tsx +1 -105
  59. package/src/cli/cmd/tui/context/kv.tsx +1 -76
  60. package/src/cli/cmd/tui/context/local.tsx +1 -478
  61. package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
  62. package/src/cli/cmd/tui/context/project.tsx +1 -109
  63. package/src/cli/cmd/tui/context/prompt.tsx +1 -18
  64. package/src/cli/cmd/tui/context/route.tsx +1 -67
  65. package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
  66. package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
  67. package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
  68. package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
  69. package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
  70. package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
  71. package/src/cli/cmd/tui/context/sdk.tsx +1 -142
  72. package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/sync.tsx +1 -713
  74. package/src/cli/cmd/tui/context/theme.tsx +1 -307
  75. package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
  76. package/src/tool/apply_patch.ts +1 -334
  77. package/src/tool/bash.ts +1 -656
  78. package/src/tool/core/external-directory.ts +55 -0
  79. package/src/tool/core/invalid.ts +21 -0
  80. package/src/tool/core/recall.ts +164 -0
  81. package/src/tool/core/recall.txt +12 -0
  82. package/src/tool/core/schema.ts +16 -0
  83. package/src/tool/core/tool.ts +162 -0
  84. package/src/tool/core/truncate.ts +160 -0
  85. package/src/tool/core/truncation-dir.ts +4 -0
  86. package/src/tool/diagnostics.ts +1 -20
  87. package/src/tool/edit-replacers.ts +1 -288
  88. package/src/tool/edit-utils.ts +1 -86
  89. package/src/tool/edit.ts +1 -262
  90. package/src/tool/external-directory.ts +1 -55
  91. package/src/tool/file/apply_patch.ts +334 -0
  92. package/src/tool/file/apply_patch.txt +33 -0
  93. package/src/tool/file/bash.ts +656 -0
  94. package/src/tool/file/bash.txt +119 -0
  95. package/src/tool/file/edit-replacers.ts +288 -0
  96. package/src/tool/file/edit-utils.ts +86 -0
  97. package/src/tool/file/edit.ts +262 -0
  98. package/src/tool/file/edit.txt +10 -0
  99. package/src/tool/file/read.ts +389 -0
  100. package/src/tool/file/read.txt +14 -0
  101. package/src/tool/file/write.ts +114 -0
  102. package/src/tool/file/write.txt +8 -0
  103. package/src/tool/glob.ts +1 -115
  104. package/src/tool/grep.ts +1 -151
  105. package/src/tool/integration/diagnostics.ts +20 -0
  106. package/src/tool/integration/lsp.ts +113 -0
  107. package/src/tool/integration/lsp.txt +24 -0
  108. package/src/tool/integration/mcp-exa.ts +73 -0
  109. package/src/tool/integration/package.ts +168 -0
  110. package/src/tool/integration/registry.ts +375 -0
  111. package/src/tool/invalid.ts +1 -21
  112. package/src/tool/lsp.ts +1 -113
  113. package/src/tool/mcp-exa.ts +1 -73
  114. package/src/tool/package.ts +1 -168
  115. package/src/tool/plan.ts +1 -30
  116. package/src/tool/question.ts +1 -52
  117. package/src/tool/read.ts +1 -389
  118. package/src/tool/recall.ts +1 -164
  119. package/src/tool/registry.ts +1 -375
  120. package/src/tool/schema.ts +1 -16
  121. package/src/tool/search/glob.ts +115 -0
  122. package/src/tool/search/glob.txt +6 -0
  123. package/src/tool/search/grep.ts +151 -0
  124. package/src/tool/search/grep.txt +8 -0
  125. package/src/tool/search/warpgrep.ts +107 -0
  126. package/src/tool/search/warpgrep.txt +10 -0
  127. package/src/tool/search/webfetch.ts +202 -0
  128. package/src/tool/search/webfetch.txt +13 -0
  129. package/src/tool/search/websearch.ts +71 -0
  130. package/src/tool/search/websearch.txt +14 -0
  131. package/src/tool/skill.ts +1 -91
  132. package/src/tool/task.ts +1 -197
  133. package/src/tool/todo.ts +1 -62
  134. package/src/tool/tool.ts +1 -162
  135. package/src/tool/truncate.ts +1 -160
  136. package/src/tool/truncation-dir.ts +1 -4
  137. package/src/tool/warpgrep.ts +1 -107
  138. package/src/tool/webfetch.ts +1 -202
  139. package/src/tool/websearch.ts +1 -71
  140. package/src/tool/workflow/plan-enter.txt +14 -0
  141. package/src/tool/workflow/plan-exit.txt +13 -0
  142. package/src/tool/workflow/plan.ts +30 -0
  143. package/src/tool/workflow/question.ts +52 -0
  144. package/src/tool/workflow/question.txt +11 -0
  145. package/src/tool/workflow/skill.ts +91 -0
  146. package/src/tool/workflow/skill.txt +5 -0
  147. package/src/tool/workflow/task.ts +197 -0
  148. package/src/tool/workflow/task.txt +57 -0
  149. package/src/tool/workflow/todo.ts +62 -0
  150. package/src/tool/workflow/todowrite.txt +167 -0
  151. package/src/tool/write.ts +1 -114
package/bin/saeeol.cjs ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("child_process")
4
+ const fs = require("fs")
5
+ const path = require("path")
6
+ const os = require("os")
7
+
8
+ function run(target) {
9
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
10
+ stdio: "inherit",
11
+ })
12
+ if (result.error) {
13
+ console.error(result.error.message)
14
+ process.exit(1)
15
+ }
16
+ const code = typeof result.status === "number" ? result.status : 0
17
+ process.exit(code)
18
+ }
19
+
20
+ const envPath = process.env.SAEEOL_BIN_PATH
21
+ if (envPath) {
22
+ run(envPath)
23
+ }
24
+
25
+ const scriptPath = fs.realpathSync(__filename)
26
+ const scriptDir = path.dirname(scriptPath)
27
+
28
+ // fall through to findBinary() if cached binary fails
29
+ const cached = path.join(scriptDir, ".saeeol")
30
+ if (fs.existsSync(cached)) {
31
+ const result = childProcess.spawnSync(cached, process.argv.slice(2), {
32
+ stdio: "inherit",
33
+ })
34
+ if (!result.error) {
35
+ const code = typeof result.status === "number" ? result.status : 0
36
+ process.exit(code)
37
+ }
38
+ // cached binary failed (e.g. wrong platform/arch, missing dynamic linker),
39
+ // fall through to findBinary() which has better variant detection
40
+ }
41
+
42
+ const platformMap = {
43
+ darwin: "darwin",
44
+ linux: "linux",
45
+ win32: "windows",
46
+ }
47
+ const archMap = {
48
+ x64: "x64",
49
+ arm64: "arm64",
50
+ arm: "arm",
51
+ }
52
+
53
+ let platform = platformMap[os.platform()]
54
+ if (!platform) {
55
+ platform = os.platform()
56
+ }
57
+ let arch = archMap[os.arch()]
58
+ if (!arch) {
59
+ arch = os.arch()
60
+ }
61
+ const base = "saeeol-" + platform + "-" + arch
62
+ const binary = platform === "windows" ? "saeeol.exe" : "saeeol"
63
+
64
+ function supportsAvx2() {
65
+ if (arch !== "x64") return false
66
+
67
+ if (platform === "linux") {
68
+ try {
69
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
70
+ } catch {
71
+ return false
72
+ }
73
+ }
74
+
75
+ if (platform === "darwin") {
76
+ try {
77
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
78
+ encoding: "utf8",
79
+ timeout: 1500,
80
+ })
81
+ if (result.status !== 0) return false
82
+ return (result.stdout || "").trim() === "1"
83
+ } catch {
84
+ return false
85
+ }
86
+ }
87
+
88
+ if (platform === "windows") {
89
+ const cmd =
90
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
91
+
92
+ for (const exe of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
93
+ try {
94
+ const result = childProcess.spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", cmd], {
95
+ encoding: "utf8",
96
+ timeout: 3000,
97
+ windowsHide: true,
98
+ })
99
+ if (result.status !== 0) continue
100
+ const out = (result.stdout || "").trim().toLowerCase()
101
+ if (out === "true" || out === "1") return true
102
+ if (out === "false" || out === "0") return false
103
+ } catch {
104
+ continue
105
+ }
106
+ }
107
+
108
+ return false
109
+ }
110
+
111
+ return false
112
+ }
113
+
114
+ const names = (() => {
115
+ const avx2 = supportsAvx2()
116
+ const baseline = arch === "x64" && !avx2
117
+
118
+ if (platform === "linux") {
119
+ const musl = (() => {
120
+ try {
121
+ if (fs.existsSync("/etc/alpine-release")) return true
122
+ } catch {
123
+ // ignore
124
+ }
125
+
126
+ try {
127
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
128
+ const text = ((result.stdout || "") + (result.stderr || "")).toLowerCase()
129
+ if (text.includes("musl")) return true
130
+ } catch {
131
+ // ignore
132
+ }
133
+
134
+ return false
135
+ })()
136
+
137
+ if (musl) {
138
+ if (arch === "x64") {
139
+ if (baseline) return [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
140
+ return [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
141
+ }
142
+ return [`${base}-musl`, base]
143
+ }
144
+
145
+ if (arch === "x64") {
146
+ if (baseline) return [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
147
+ return [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
148
+ }
149
+ return [base, `${base}-musl`]
150
+ }
151
+
152
+ if (arch === "x64") {
153
+ if (baseline) return [`${base}-baseline`, base]
154
+ return [base, `${base}-baseline`]
155
+ }
156
+ return [base]
157
+ })()
158
+
159
+ function findBinary(startDir) {
160
+ let current = startDir
161
+ for (;;) {
162
+ const modules = path.join(current, "node_modules")
163
+ if (fs.existsSync(modules)) {
164
+ for (const name of names) {
165
+ const candidate = path.join(modules, name, "bin", binary)
166
+ if (fs.existsSync(candidate)) return candidate
167
+ }
168
+ }
169
+ const parent = path.dirname(current)
170
+ if (parent === current) {
171
+ return
172
+ }
173
+ current = parent
174
+ }
175
+ }
176
+
177
+ const resolved = findBinary(scriptDir)
178
+ if (!resolved) {
179
+ console.error(
180
+ "It seems that your package manager failed to install the right version of the SAEEOL CLI for your platform. You can try manually installing " +
181
+ names.map((n) => `\"${n}\"`).join(" or ") +
182
+ " package",
183
+ )
184
+ process.exit(1)
185
+ }
186
+
187
+ run(resolved)
package/npm/bin/saeeol CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "name": "saeeol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -24,7 +24,7 @@
24
24
  "db": "bun drizzle-kit"
25
25
  },
26
26
  "bin": {
27
- "saeeol": "./bin/saeeol"
27
+ "saeeol": "./bin/saeeol.cjs"
28
28
  },
29
29
  "exports": {
30
30
  "./*": "./src/*.ts"
@@ -50,8 +50,8 @@
50
50
  "@babel/core": "7.28.4",
51
51
  "@effect/language-service": "0.84.2",
52
52
  "@octokit/webhooks-types": "7.6.1",
53
- "@saeeol/script": "7.3.1",
54
- "@saeeol/core": "7.3.1",
53
+ "@saeeol/script": "7.3.2",
54
+ "@saeeol/core": "7.3.2",
55
55
  "@parcel/watcher-darwin-arm64": "2.5.1",
56
56
  "@parcel/watcher-darwin-x64": "2.5.1",
57
57
  "@parcel/watcher-linux-arm64-glibc": "2.5.1",
@@ -133,14 +133,14 @@
133
133
  "@parcel/watcher": "2.5.1",
134
134
  "@pierre/diffs": "1.1.0-beta.18",
135
135
  "@saeeol/boxes": "0.2.0",
136
- "@saeeol/core": "7.3.1",
137
- "@saeeol/gateway": "7.3.1",
138
- "@saeeol/i18n": "7.3.1",
139
- "@saeeol/indexing": "7.3.1",
140
- "@saeeol/plugin": "7.3.1",
141
- "@saeeol/script": "7.3.1",
142
- "@saeeol/sdk": "7.3.1",
143
- "@saeeol/telemetry": "7.3.1",
136
+ "@saeeol/core": "7.3.2",
137
+ "@saeeol/gateway": "7.3.2",
138
+ "@saeeol/i18n": "7.3.2",
139
+ "@saeeol/indexing": "7.3.2",
140
+ "@saeeol/plugin": "7.3.3",
141
+ "@saeeol/script": "7.3.2",
142
+ "@saeeol/sdk": "7.3.3",
143
+ "@saeeol/telemetry": "7.3.2",
144
144
  "@solid-primitives/event-bus": "1.1.2",
145
145
  "@solid-primitives/scheduled": "1.5.2",
146
146
  "@standard-schema/spec": "1.0.0",
@@ -0,0 +1,32 @@
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
+ }
@@ -0,0 +1,190 @@
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
+ }
@@ -0,0 +1,103 @@
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
+ }