cerebras-cli 1.0.0

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 (314) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +10 -0
  3. package/README.md +15 -0
  4. package/bin/opencode +84 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +128 -0
  7. package/parsers-config.ts +239 -0
  8. package/script/build.ts +151 -0
  9. package/script/postinstall.mjs +122 -0
  10. package/script/publish.ts +256 -0
  11. package/script/schema.ts +47 -0
  12. package/src/acp/README.md +164 -0
  13. package/src/acp/agent.ts +812 -0
  14. package/src/acp/session.ts +70 -0
  15. package/src/acp/types.ts +22 -0
  16. package/src/agent/agent.ts +310 -0
  17. package/src/agent/generate.txt +75 -0
  18. package/src/auth/index.ts +70 -0
  19. package/src/bun/index.ts +152 -0
  20. package/src/bus/global.ts +10 -0
  21. package/src/bus/index.ts +142 -0
  22. package/src/cli/bootstrap.ts +17 -0
  23. package/src/cli/cmd/acp.ts +88 -0
  24. package/src/cli/cmd/agent.ts +165 -0
  25. package/src/cli/cmd/auth.ts +369 -0
  26. package/src/cli/cmd/cmd.ts +7 -0
  27. package/src/cli/cmd/debug/config.ts +15 -0
  28. package/src/cli/cmd/debug/file.ts +91 -0
  29. package/src/cli/cmd/debug/index.ts +41 -0
  30. package/src/cli/cmd/debug/lsp.ts +47 -0
  31. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  32. package/src/cli/cmd/debug/scrap.ts +15 -0
  33. package/src/cli/cmd/debug/snapshot.ts +48 -0
  34. package/src/cli/cmd/export.ts +88 -0
  35. package/src/cli/cmd/generate.ts +38 -0
  36. package/src/cli/cmd/github.ts +1200 -0
  37. package/src/cli/cmd/import.ts +98 -0
  38. package/src/cli/cmd/mcp.ts +400 -0
  39. package/src/cli/cmd/models.ts +77 -0
  40. package/src/cli/cmd/pr.ts +112 -0
  41. package/src/cli/cmd/run.ts +342 -0
  42. package/src/cli/cmd/serve.ts +31 -0
  43. package/src/cli/cmd/session.ts +106 -0
  44. package/src/cli/cmd/stats.ts +298 -0
  45. package/src/cli/cmd/tui/app.tsx +732 -0
  46. package/src/cli/cmd/tui/attach.ts +25 -0
  47. package/src/cli/cmd/tui/component/border.tsx +21 -0
  48. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  49. package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
  50. package/src/cli/cmd/tui/component/dialog-feedback.tsx +160 -0
  51. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  52. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  53. package/src/cli/cmd/tui/component/dialog-notification.tsx +78 -0
  54. package/src/cli/cmd/tui/component/dialog-provider.tsx +222 -0
  55. package/src/cli/cmd/tui/component/dialog-session-list.tsx +97 -0
  56. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  57. package/src/cli/cmd/tui/component/dialog-status.tsx +114 -0
  58. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  59. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  60. package/src/cli/cmd/tui/component/logo.tsx +37 -0
  61. package/src/cli/cmd/tui/component/notification-banner.tsx +58 -0
  62. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +530 -0
  63. package/src/cli/cmd/tui/component/prompt/history.tsx +107 -0
  64. package/src/cli/cmd/tui/component/prompt/index.tsx +931 -0
  65. package/src/cli/cmd/tui/context/args.tsx +14 -0
  66. package/src/cli/cmd/tui/context/directory.ts +12 -0
  67. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  68. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  69. package/src/cli/cmd/tui/context/keybind.tsx +111 -0
  70. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  71. package/src/cli/cmd/tui/context/local.tsx +339 -0
  72. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/route.tsx +45 -0
  74. package/src/cli/cmd/tui/context/sdk.tsx +75 -0
  75. package/src/cli/cmd/tui/context/sync.tsx +374 -0
  76. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  77. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  78. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  79. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  80. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  81. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  82. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  83. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  84. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  85. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  86. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  87. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  88. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  89. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  90. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  91. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  92. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  93. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  94. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  95. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  96. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  97. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  98. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  99. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  100. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  101. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  102. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  103. package/src/cli/cmd/tui/context/theme.tsx +1077 -0
  104. package/src/cli/cmd/tui/event.ts +39 -0
  105. package/src/cli/cmd/tui/routes/home.tsx +104 -0
  106. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +93 -0
  107. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +37 -0
  108. package/src/cli/cmd/tui/routes/session/footer.tsx +76 -0
  109. package/src/cli/cmd/tui/routes/session/header.tsx +183 -0
  110. package/src/cli/cmd/tui/routes/session/index.tsx +1703 -0
  111. package/src/cli/cmd/tui/routes/session/sidebar.tsx +586 -0
  112. package/src/cli/cmd/tui/spawn.ts +60 -0
  113. package/src/cli/cmd/tui/thread.ts +120 -0
  114. package/src/cli/cmd/tui/ui/dialog-alert.tsx +55 -0
  115. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +81 -0
  116. package/src/cli/cmd/tui/ui/dialog-help.tsx +36 -0
  117. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +75 -0
  118. package/src/cli/cmd/tui/ui/dialog-select.tsx +317 -0
  119. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  120. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  121. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  122. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  123. package/src/cli/cmd/tui/util/editor.ts +32 -0
  124. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  125. package/src/cli/cmd/tui/worker.ts +63 -0
  126. package/src/cli/cmd/uninstall.ts +344 -0
  127. package/src/cli/cmd/upgrade.ts +67 -0
  128. package/src/cli/cmd/web.ts +84 -0
  129. package/src/cli/error.ts +55 -0
  130. package/src/cli/ui.ts +84 -0
  131. package/src/cli/upgrade.ts +25 -0
  132. package/src/command/index.ts +79 -0
  133. package/src/command/template/initialize.txt +10 -0
  134. package/src/command/template/review.txt +73 -0
  135. package/src/config/config.ts +886 -0
  136. package/src/config/markdown.ts +41 -0
  137. package/src/env/index.ts +26 -0
  138. package/src/file/fzf.ts +124 -0
  139. package/src/file/ignore.ts +83 -0
  140. package/src/file/index.ts +326 -0
  141. package/src/file/ripgrep.ts +391 -0
  142. package/src/file/time.ts +38 -0
  143. package/src/file/watcher.ts +89 -0
  144. package/src/flag/flag.ts +28 -0
  145. package/src/format/formatter.ts +277 -0
  146. package/src/format/index.ts +137 -0
  147. package/src/global/index.ts +52 -0
  148. package/src/id/id.ts +73 -0
  149. package/src/ide/index.ts +75 -0
  150. package/src/index.ts +158 -0
  151. package/src/installation/index.ts +194 -0
  152. package/src/lsp/client.ts +215 -0
  153. package/src/lsp/index.ts +370 -0
  154. package/src/lsp/language.ts +111 -0
  155. package/src/lsp/server.ts +1327 -0
  156. package/src/mcp/auth.ts +82 -0
  157. package/src/mcp/index.ts +576 -0
  158. package/src/mcp/oauth-callback.ts +203 -0
  159. package/src/mcp/oauth-provider.ts +132 -0
  160. package/src/notification/index.ts +101 -0
  161. package/src/patch/index.ts +622 -0
  162. package/src/permission/index.ts +198 -0
  163. package/src/plugin/index.ts +95 -0
  164. package/src/project/bootstrap.ts +31 -0
  165. package/src/project/instance.ts +68 -0
  166. package/src/project/project.ts +133 -0
  167. package/src/project/state.ts +65 -0
  168. package/src/project/vcs.ts +77 -0
  169. package/src/provider/auth.ts +143 -0
  170. package/src/provider/models-macro.ts +11 -0
  171. package/src/provider/models.ts +93 -0
  172. package/src/provider/provider.ts +996 -0
  173. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  174. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  175. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  176. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  177. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +27 -0
  178. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  179. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  180. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  181. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  182. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  183. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  184. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  185. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  190. package/src/provider/transform.ts +406 -0
  191. package/src/pty/index.ts +226 -0
  192. package/src/ratelimit/index.ts +185 -0
  193. package/src/server/error.ts +36 -0
  194. package/src/server/project.ts +50 -0
  195. package/src/server/server.ts +2463 -0
  196. package/src/server/tui.ts +71 -0
  197. package/src/session/compaction.ts +257 -0
  198. package/src/session/index.ts +470 -0
  199. package/src/session/message-v2.ts +641 -0
  200. package/src/session/message.ts +189 -0
  201. package/src/session/processor.ts +443 -0
  202. package/src/session/prompt/anthropic-20250930.txt +166 -0
  203. package/src/session/prompt/anthropic.txt +105 -0
  204. package/src/session/prompt/anthropic_spoof.txt +1 -0
  205. package/src/session/prompt/beast.txt +147 -0
  206. package/src/session/prompt/build-switch.txt +5 -0
  207. package/src/session/prompt/codex.txt +318 -0
  208. package/src/session/prompt/compaction.txt +12 -0
  209. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  210. package/src/session/prompt/gemini.txt +155 -0
  211. package/src/session/prompt/max-steps.txt +16 -0
  212. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  213. package/src/session/prompt/plan.txt +26 -0
  214. package/src/session/prompt/polaris.txt +107 -0
  215. package/src/session/prompt/qwen.txt +109 -0
  216. package/src/session/prompt/summarize.txt +4 -0
  217. package/src/session/prompt/title.txt +36 -0
  218. package/src/session/prompt.ts +1541 -0
  219. package/src/session/retry.ts +82 -0
  220. package/src/session/revert.ts +108 -0
  221. package/src/session/status.ts +75 -0
  222. package/src/session/summary.ts +203 -0
  223. package/src/session/system.ts +148 -0
  224. package/src/session/todo.ts +36 -0
  225. package/src/share/share-next.ts +195 -0
  226. package/src/share/share.ts +87 -0
  227. package/src/snapshot/index.ts +197 -0
  228. package/src/storage/storage.ts +226 -0
  229. package/src/telemetry/index.ts +232 -0
  230. package/src/tool/bash.ts +365 -0
  231. package/src/tool/bash.txt +128 -0
  232. package/src/tool/batch.ts +173 -0
  233. package/src/tool/batch.txt +28 -0
  234. package/src/tool/codesearch.ts +138 -0
  235. package/src/tool/codesearch.txt +12 -0
  236. package/src/tool/edit.ts +674 -0
  237. package/src/tool/edit.txt +10 -0
  238. package/src/tool/glob.ts +65 -0
  239. package/src/tool/glob.txt +6 -0
  240. package/src/tool/grep.ts +120 -0
  241. package/src/tool/grep.txt +8 -0
  242. package/src/tool/invalid.ts +17 -0
  243. package/src/tool/ls.ts +110 -0
  244. package/src/tool/ls.txt +1 -0
  245. package/src/tool/lsp-diagnostics.ts +26 -0
  246. package/src/tool/lsp-diagnostics.txt +1 -0
  247. package/src/tool/lsp-hover.ts +31 -0
  248. package/src/tool/lsp-hover.txt +1 -0
  249. package/src/tool/multiedit.ts +46 -0
  250. package/src/tool/multiedit.txt +41 -0
  251. package/src/tool/patch.ts +233 -0
  252. package/src/tool/patch.txt +1 -0
  253. package/src/tool/read.ts +217 -0
  254. package/src/tool/read.txt +12 -0
  255. package/src/tool/registry.ts +148 -0
  256. package/src/tool/task.ts +135 -0
  257. package/src/tool/task.txt +60 -0
  258. package/src/tool/todo.ts +39 -0
  259. package/src/tool/todoread.txt +14 -0
  260. package/src/tool/todowrite.txt +167 -0
  261. package/src/tool/tool.ts +66 -0
  262. package/src/tool/webfetch.ts +187 -0
  263. package/src/tool/webfetch.txt +14 -0
  264. package/src/tool/websearch.ts +150 -0
  265. package/src/tool/websearch.txt +11 -0
  266. package/src/tool/write.ts +99 -0
  267. package/src/tool/write.txt +8 -0
  268. package/src/types/shims.d.ts +3 -0
  269. package/src/util/color.ts +19 -0
  270. package/src/util/context.ts +25 -0
  271. package/src/util/defer.ts +12 -0
  272. package/src/util/eventloop.ts +20 -0
  273. package/src/util/filesystem.ts +69 -0
  274. package/src/util/fn.ts +11 -0
  275. package/src/util/iife.ts +3 -0
  276. package/src/util/keybind.ts +79 -0
  277. package/src/util/lazy.ts +11 -0
  278. package/src/util/locale.ts +81 -0
  279. package/src/util/lock.ts +98 -0
  280. package/src/util/log.ts +177 -0
  281. package/src/util/queue.ts +32 -0
  282. package/src/util/rpc.ts +42 -0
  283. package/src/util/scrap.ts +10 -0
  284. package/src/util/signal.ts +12 -0
  285. package/src/util/timeout.ts +14 -0
  286. package/src/util/token.ts +7 -0
  287. package/src/util/wildcard.ts +54 -0
  288. package/sst-env.d.ts +9 -0
  289. package/test/bun.test.ts +53 -0
  290. package/test/config/agent-color.test.ts +66 -0
  291. package/test/config/config.test.ts +503 -0
  292. package/test/config/markdown.test.ts +89 -0
  293. package/test/file/ignore.test.ts +10 -0
  294. package/test/fixture/fixture.ts +28 -0
  295. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  296. package/test/ide/ide.test.ts +82 -0
  297. package/test/keybind.test.ts +317 -0
  298. package/test/lsp/client.test.ts +95 -0
  299. package/test/patch/patch.test.ts +348 -0
  300. package/test/preload.ts +38 -0
  301. package/test/project/project.test.ts +42 -0
  302. package/test/provider/provider.test.ts +1809 -0
  303. package/test/provider/transform.test.ts +305 -0
  304. package/test/session/retry.test.ts +61 -0
  305. package/test/session/session.test.ts +71 -0
  306. package/test/snapshot/snapshot.test.ts +939 -0
  307. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  308. package/test/tool/bash.test.ts +55 -0
  309. package/test/tool/patch.test.ts +259 -0
  310. package/test/util/iife.test.ts +36 -0
  311. package/test/util/lazy.test.ts +50 -0
  312. package/test/util/timeout.test.ts +21 -0
  313. package/test/util/wildcard.test.ts +55 -0
  314. package/tsconfig.json +17 -0
@@ -0,0 +1,120 @@
1
+ import { cmd } from "@/cli/cmd/cmd"
2
+ import { tui } from "./app"
3
+ import { Rpc } from "@/util/rpc"
4
+ import { type rpc } from "./worker"
5
+ import path from "path"
6
+ import { UI } from "@/cli/ui"
7
+ import { iife } from "@/util/iife"
8
+ import { Log } from "@/util/log"
9
+
10
+ declare global {
11
+ const OPENCODE_WORKER_PATH: string
12
+ }
13
+
14
+ export const TuiThreadCommand = cmd({
15
+ command: "$0 [project]",
16
+ describe: "start opencode tui",
17
+ builder: (yargs) =>
18
+ yargs
19
+ .positional("project", {
20
+ type: "string",
21
+ describe: "path to start opencode in",
22
+ })
23
+ .option("model", {
24
+ type: "string",
25
+ alias: ["m"],
26
+ describe: "model to use in the format of provider/model",
27
+ })
28
+ .option("continue", {
29
+ alias: ["c"],
30
+ describe: "continue the last session",
31
+ type: "boolean",
32
+ })
33
+ .option("session", {
34
+ alias: ["s"],
35
+ type: "string",
36
+ describe: "session id to continue",
37
+ })
38
+ .option("prompt", {
39
+ alias: ["p"],
40
+ type: "string",
41
+ describe: "prompt to use",
42
+ })
43
+ .option("agent", {
44
+ type: "string",
45
+ describe: "agent to use",
46
+ })
47
+ .option("port", {
48
+ type: "number",
49
+ describe: "port to listen on",
50
+ default: 0,
51
+ })
52
+ .option("hostname", {
53
+ type: "string",
54
+ describe: "hostname to listen on",
55
+ default: "127.0.0.1",
56
+ }),
57
+ handler: async (args) => {
58
+ // Resolve relative paths against PWD to preserve behavior when using --cwd flag
59
+ const baseCwd = process.env.PWD ?? process.cwd()
60
+ const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
61
+ const localWorker = new URL("./worker.ts", import.meta.url)
62
+ const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
63
+ const workerPath = await iife(async () => {
64
+ if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
65
+ if (await Bun.file(distWorker).exists()) return distWorker
66
+ return localWorker
67
+ })
68
+ try {
69
+ process.chdir(cwd)
70
+ } catch (e) {
71
+ UI.error("Failed to change directory to " + cwd)
72
+ return
73
+ }
74
+
75
+ const worker = new Worker(workerPath, {
76
+ env: Object.fromEntries(
77
+ Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
78
+ ),
79
+ })
80
+ worker.onerror = (e) => {
81
+ Log.Default.error(e)
82
+ }
83
+ const client = Rpc.client<typeof rpc>(worker)
84
+ process.on("uncaughtException", (e) => {
85
+ Log.Default.error(e)
86
+ })
87
+ process.on("unhandledRejection", (e) => {
88
+ Log.Default.error(e)
89
+ })
90
+ const server = await client.call("server", {
91
+ port: args.port,
92
+ hostname: args.hostname,
93
+ })
94
+ const prompt = await iife(async () => {
95
+ const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
96
+ if (!args.prompt) return piped
97
+ return piped ? piped + "\n" + args.prompt : args.prompt
98
+ })
99
+
100
+ const tuiPromise = tui({
101
+ url: server.url,
102
+ args: {
103
+ continue: args.continue,
104
+ sessionID: args.session,
105
+ agent: args.agent,
106
+ model: args.model,
107
+ prompt,
108
+ },
109
+ onExit: async () => {
110
+ await client.call("shutdown", undefined)
111
+ },
112
+ })
113
+
114
+ setTimeout(() => {
115
+ client.call("checkUpgrade", { directory: cwd }).catch(() => {})
116
+ }, 1000)
117
+
118
+ await tuiPromise
119
+ },
120
+ })
@@ -0,0 +1,55 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import { useTheme } from "../context/theme"
3
+ import { useDialog, type DialogContext } from "./dialog"
4
+ import { useKeyboard } from "@opentui/solid"
5
+
6
+ export type DialogAlertProps = {
7
+ title: string
8
+ message: string
9
+ onConfirm?: () => void
10
+ }
11
+
12
+ export function DialogAlert(props: DialogAlertProps) {
13
+ const dialog = useDialog()
14
+ const { theme } = useTheme()
15
+
16
+ useKeyboard((evt) => {
17
+ if (evt.name === "return") {
18
+ props.onConfirm?.()
19
+ dialog.clear()
20
+ }
21
+ })
22
+ return (
23
+ <box paddingLeft={2} paddingRight={2} gap={1}>
24
+ <box flexDirection="row" justifyContent="space-between">
25
+ <text attributes={TextAttributes.BOLD}>{props.title}</text>
26
+ <text fg={theme.textMuted}>esc</text>
27
+ </box>
28
+ <box paddingBottom={1}>
29
+ <text fg={theme.textMuted}>{props.message}</text>
30
+ </box>
31
+ <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
32
+ <box
33
+ paddingLeft={3}
34
+ paddingRight={3}
35
+ backgroundColor={theme.primary}
36
+ onMouseUp={() => {
37
+ props.onConfirm?.()
38
+ dialog.clear()
39
+ }}
40
+ >
41
+ <text fg={theme.selectedListItemText}>ok</text>
42
+ </box>
43
+ </box>
44
+ </box>
45
+ )
46
+ }
47
+
48
+ DialogAlert.show = (dialog: DialogContext, title: string, message: string) => {
49
+ return new Promise<void>((resolve) => {
50
+ dialog.replace(
51
+ () => <DialogAlert title={title} message={message} onConfirm={() => resolve()} />,
52
+ () => resolve(),
53
+ )
54
+ })
55
+ }
@@ -0,0 +1,81 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import { useTheme } from "../context/theme"
3
+ import { useDialog, type DialogContext } from "./dialog"
4
+ import { createStore } from "solid-js/store"
5
+ import { For } from "solid-js"
6
+ import { useKeyboard } from "@opentui/solid"
7
+ import { Locale } from "@/util/locale"
8
+
9
+ export type DialogConfirmProps = {
10
+ title: string
11
+ message: string
12
+ onConfirm?: () => void
13
+ onCancel?: () => void
14
+ }
15
+
16
+ export function DialogConfirm(props: DialogConfirmProps) {
17
+ const dialog = useDialog()
18
+ const { theme } = useTheme()
19
+ const [store, setStore] = createStore({
20
+ active: "confirm" as "confirm" | "cancel",
21
+ })
22
+
23
+ useKeyboard((evt) => {
24
+ if (evt.name === "return") {
25
+ if (store.active === "confirm") props.onConfirm?.()
26
+ if (store.active === "cancel") props.onCancel?.()
27
+ dialog.clear()
28
+ }
29
+
30
+ if (evt.name === "left" || evt.name === "right") {
31
+ setStore("active", store.active === "confirm" ? "cancel" : "confirm")
32
+ }
33
+ })
34
+ return (
35
+ <box paddingLeft={2} paddingRight={2} gap={1}>
36
+ <box flexDirection="row" justifyContent="space-between">
37
+ <text attributes={TextAttributes.BOLD}>{props.title}</text>
38
+ <text fg={theme.textMuted}>esc</text>
39
+ </box>
40
+ <box paddingBottom={1}>
41
+ <text fg={theme.textMuted}>{props.message}</text>
42
+ </box>
43
+ <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
44
+ <For each={["cancel", "confirm"]}>
45
+ {(key) => (
46
+ <box
47
+ paddingLeft={1}
48
+ paddingRight={1}
49
+ backgroundColor={key === store.active ? theme.primary : undefined}
50
+ onMouseUp={(evt) => {
51
+ if (key === "confirm") props.onConfirm?.()
52
+ if (key === "cancel") props.onCancel?.()
53
+ dialog.clear()
54
+ }}
55
+ >
56
+ <text fg={key === store.active ? theme.selectedListItemText : theme.textMuted}>
57
+ {Locale.titlecase(key)}
58
+ </text>
59
+ </box>
60
+ )}
61
+ </For>
62
+ </box>
63
+ </box>
64
+ )
65
+ }
66
+
67
+ DialogConfirm.show = (dialog: DialogContext, title: string, message: string) => {
68
+ return new Promise<boolean>((resolve) => {
69
+ dialog.replace(
70
+ () => (
71
+ <DialogConfirm
72
+ title={title}
73
+ message={message}
74
+ onConfirm={() => resolve(true)}
75
+ onCancel={() => resolve(false)}
76
+ />
77
+ ),
78
+ () => resolve(false),
79
+ )
80
+ })
81
+ }
@@ -0,0 +1,36 @@
1
+ import { TextAttributes } from "@opentui/core"
2
+ import { useTheme } from "@tui/context/theme"
3
+ import { useDialog } from "./dialog"
4
+ import { useKeyboard } from "@opentui/solid"
5
+ import { useKeybind } from "@tui/context/keybind"
6
+
7
+ export function DialogHelp() {
8
+ const dialog = useDialog()
9
+ const { theme } = useTheme()
10
+ const keybind = useKeybind()
11
+
12
+ useKeyboard((evt) => {
13
+ if (evt.name === "return" || evt.name === "escape") {
14
+ dialog.clear()
15
+ }
16
+ })
17
+
18
+ return (
19
+ <box paddingLeft={2} paddingRight={2} gap={1}>
20
+ <box flexDirection="row" justifyContent="space-between">
21
+ <text attributes={TextAttributes.BOLD}>Help</text>
22
+ <text fg={theme.textMuted}>esc/enter</text>
23
+ </box>
24
+ <box paddingBottom={1}>
25
+ <text fg={theme.textMuted}>
26
+ Press {keybind.print("command_list")} to see all available actions and commands in any context.
27
+ </text>
28
+ </box>
29
+ <box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
30
+ <box paddingLeft={3} paddingRight={3} backgroundColor={theme.primary} onMouseUp={() => dialog.clear()}>
31
+ <text fg={theme.selectedListItemText}>ok</text>
32
+ </box>
33
+ </box>
34
+ </box>
35
+ )
36
+ }
@@ -0,0 +1,75 @@
1
+ import { TextareaRenderable, TextAttributes } from "@opentui/core"
2
+ import { useTheme } from "../context/theme"
3
+ import { useDialog, type DialogContext } from "./dialog"
4
+ import { onMount, type JSX } from "solid-js"
5
+ import { useKeyboard } from "@opentui/solid"
6
+
7
+ export type DialogPromptProps = {
8
+ title: string
9
+ description?: () => JSX.Element
10
+ placeholder?: string
11
+ value?: string
12
+ onConfirm?: (value: string) => void
13
+ onCancel?: () => void
14
+ }
15
+
16
+ export function DialogPrompt(props: DialogPromptProps) {
17
+ const dialog = useDialog()
18
+ const { theme } = useTheme()
19
+ let textarea: TextareaRenderable
20
+
21
+ useKeyboard((evt) => {
22
+ if (evt.name === "return") {
23
+ props.onConfirm?.(textarea.plainText)
24
+ }
25
+ })
26
+
27
+ onMount(() => {
28
+ dialog.setSize("medium")
29
+ setTimeout(() => {
30
+ textarea.focus()
31
+ }, 1)
32
+ textarea.gotoLineEnd()
33
+ })
34
+
35
+ return (
36
+ <box paddingLeft={2} paddingRight={2} gap={1}>
37
+ <box flexDirection="row" justifyContent="space-between">
38
+ <text attributes={TextAttributes.BOLD}>{props.title}</text>
39
+ <text fg={theme.textMuted}>esc</text>
40
+ </box>
41
+ <box gap={1}>
42
+ {props.description}
43
+ <textarea
44
+ onSubmit={() => {
45
+ props.onConfirm?.(textarea.plainText)
46
+ }}
47
+ height={3}
48
+ keyBindings={[{ name: "return", action: "submit" }]}
49
+ ref={(val: TextareaRenderable) => (textarea = val)}
50
+ initialValue={props.value}
51
+ placeholder={props.placeholder ?? "Enter text"}
52
+ textColor={theme.text}
53
+ focusedTextColor={theme.text}
54
+ cursorColor={theme.text}
55
+ />
56
+ </box>
57
+ <box paddingBottom={1} gap={1} flexDirection="row">
58
+ <text fg={theme.text}>
59
+ enter <span style={{ fg: theme.textMuted }}>submit</span>
60
+ </text>
61
+ </box>
62
+ </box>
63
+ )
64
+ }
65
+
66
+ DialogPrompt.show = (dialog: DialogContext, title: string, options?: Omit<DialogPromptProps, "title">) => {
67
+ return new Promise<string | null>((resolve) => {
68
+ dialog.replace(
69
+ () => (
70
+ <DialogPrompt title={title} {...options} onConfirm={(value) => resolve(value)} onCancel={() => resolve(null)} />
71
+ ),
72
+ () => resolve(null),
73
+ )
74
+ })
75
+ }
@@ -0,0 +1,317 @@
1
+ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
2
+ import { useTheme, selectedForeground } from "@tui/context/theme"
3
+ import { entries, filter, flatMap, groupBy, pipe, take } from "remeda"
4
+ import { batch, createEffect, createMemo, For, Show, type JSX } from "solid-js"
5
+ import { createStore } from "solid-js/store"
6
+ import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
7
+ import * as fuzzysort from "fuzzysort"
8
+ import { isDeepEqual } from "remeda"
9
+ import { useDialog, type DialogContext } from "@tui/ui/dialog"
10
+ import { useKeybind } from "@tui/context/keybind"
11
+ import { Keybind } from "@/util/keybind"
12
+ import { Locale } from "@/util/locale"
13
+
14
+ export interface DialogSelectProps<T> {
15
+ title: string
16
+ placeholder?: string
17
+ options: DialogSelectOption<T>[]
18
+ ref?: (ref: DialogSelectRef<T>) => void
19
+ onMove?: (option: DialogSelectOption<T>) => void
20
+ onFilter?: (query: string) => void
21
+ onSelect?: (option: DialogSelectOption<T>) => void
22
+ keybind?: {
23
+ keybind: Keybind.Info
24
+ title: string
25
+ disabled?: boolean
26
+ onTrigger: (option: DialogSelectOption<T>) => void
27
+ }[]
28
+ current?: T
29
+ }
30
+
31
+ export interface DialogSelectOption<T = any> {
32
+ title: string
33
+ value: T
34
+ description?: string
35
+ footer?: JSX.Element | string
36
+ category?: string
37
+ disabled?: boolean
38
+ bg?: RGBA
39
+ onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
40
+ }
41
+
42
+ export type DialogSelectRef<T> = {
43
+ filter: string
44
+ filtered: DialogSelectOption<T>[]
45
+ }
46
+
47
+ export function DialogSelect<T>(props: DialogSelectProps<T>) {
48
+ const dialog = useDialog()
49
+ const { theme } = useTheme()
50
+ const [store, setStore] = createStore({
51
+ selected: 0,
52
+ filter: "",
53
+ })
54
+
55
+ createEffect(() => {
56
+ if (props.current) {
57
+ const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, props.current))
58
+ if (currentIndex >= 0) {
59
+ setStore("selected", currentIndex)
60
+ }
61
+ }
62
+ })
63
+
64
+ let input: InputRenderable
65
+
66
+ const filtered = createMemo(() => {
67
+ const needle = store.filter.toLowerCase()
68
+ const result = pipe(
69
+ props.options,
70
+ filter((x) => x.disabled !== true),
71
+ (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
72
+ )
73
+ return result
74
+ })
75
+
76
+ const grouped = createMemo(() => {
77
+ const result = pipe(
78
+ filtered(),
79
+ groupBy((x) => x.category ?? ""),
80
+ // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
81
+ entries(),
82
+ )
83
+ return result
84
+ })
85
+
86
+ const flat = createMemo(() => {
87
+ return pipe(
88
+ grouped(),
89
+ flatMap(([_, options]) => options),
90
+ )
91
+ })
92
+
93
+ const dimensions = useTerminalDimensions()
94
+ const height = createMemo(() =>
95
+ Math.min(flat().length + grouped().length * 2 - 1, Math.floor(dimensions().height / 2) - 6),
96
+ )
97
+
98
+ const selected = createMemo(() => flat()[store.selected])
99
+
100
+ createEffect(() => {
101
+ store.filter
102
+ if (store.filter.length > 0) {
103
+ setStore("selected", 0)
104
+ } else if (props.current) {
105
+ const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, props.current))
106
+ if (currentIndex >= 0) {
107
+ setStore("selected", currentIndex)
108
+ }
109
+ }
110
+ scroll.scrollTo(0)
111
+ })
112
+
113
+ function move(direction: number) {
114
+ let next = store.selected + direction
115
+ if (next < 0) next = flat().length - 1
116
+ if (next >= flat().length) next = 0
117
+ moveTo(next)
118
+ }
119
+
120
+ function moveTo(next: number) {
121
+ setStore("selected", next)
122
+ props.onMove?.(selected()!)
123
+ const target = scroll.getChildren().find((child) => {
124
+ return child.id === JSON.stringify(selected()?.value)
125
+ })
126
+ if (!target) return
127
+ const y = target.y - scroll.y
128
+ if (y >= scroll.height) {
129
+ scroll.scrollBy(y - scroll.height + 1)
130
+ }
131
+ if (y < 0) {
132
+ scroll.scrollBy(y)
133
+ if (isDeepEqual(flat()[0].value, selected()?.value)) {
134
+ scroll.scrollTo(0)
135
+ }
136
+ }
137
+ }
138
+
139
+ const keybind = useKeybind()
140
+ useKeyboard((evt) => {
141
+ if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
142
+ if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
143
+ if (evt.name === "pageup") move(-10)
144
+ if (evt.name === "pagedown") move(10)
145
+ if (evt.name === "return") {
146
+ const option = selected()
147
+ if (option) {
148
+ // evt.preventDefault()
149
+ if (option.onSelect) option.onSelect(dialog)
150
+ props.onSelect?.(option)
151
+ }
152
+ }
153
+
154
+ for (const item of props.keybind ?? []) {
155
+ if (item.disabled) continue
156
+ if (Keybind.match(item.keybind, keybind.parse(evt))) {
157
+ const s = selected()
158
+ if (s) {
159
+ evt.preventDefault()
160
+ item.onTrigger(s)
161
+ }
162
+ }
163
+ }
164
+ })
165
+
166
+ let scroll: ScrollBoxRenderable
167
+ const ref: DialogSelectRef<T> = {
168
+ get filter() {
169
+ return store.filter
170
+ },
171
+ get filtered() {
172
+ return filtered()
173
+ },
174
+ }
175
+ props.ref?.(ref)
176
+
177
+ const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
178
+
179
+ return (
180
+ <box gap={1} paddingBottom={1}>
181
+ <box paddingLeft={4} paddingRight={4}>
182
+ <box flexDirection="row" justifyContent="space-between">
183
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
184
+ {props.title}
185
+ </text>
186
+ <text fg={theme.textMuted}>esc</text>
187
+ </box>
188
+ <box paddingTop={1} paddingBottom={1}>
189
+ <input
190
+ onInput={(e) => {
191
+ batch(() => {
192
+ setStore("filter", e)
193
+ props.onFilter?.(e)
194
+ })
195
+ }}
196
+ focusedBackgroundColor={theme.backgroundPanel}
197
+ cursorColor={theme.primary}
198
+ focusedTextColor={theme.textMuted}
199
+ ref={(r) => {
200
+ input = r
201
+ setTimeout(() => input.focus(), 1)
202
+ }}
203
+ placeholder={props.placeholder ?? "Search"}
204
+ />
205
+ </box>
206
+ </box>
207
+ <scrollbox
208
+ paddingLeft={1}
209
+ paddingRight={1}
210
+ scrollbarOptions={{ visible: false }}
211
+ ref={(r: ScrollBoxRenderable) => (scroll = r)}
212
+ maxHeight={height()}
213
+ >
214
+ <For each={grouped()}>
215
+ {([category, options], index) => (
216
+ <>
217
+ <Show when={category}>
218
+ <box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
219
+ <text fg={theme.accent} attributes={TextAttributes.BOLD}>
220
+ {category}
221
+ </text>
222
+ </box>
223
+ </Show>
224
+ <For each={options}>
225
+ {(option) => {
226
+ const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
227
+ const current = createMemo(() => isDeepEqual(option.value, props.current))
228
+ return (
229
+ <box
230
+ id={JSON.stringify(option.value)}
231
+ flexDirection="row"
232
+ onMouseUp={() => {
233
+ option.onSelect?.(dialog)
234
+ props.onSelect?.(option)
235
+ }}
236
+ onMouseOver={() => {
237
+ const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
238
+ if (index === -1) return
239
+ moveTo(index)
240
+ }}
241
+ backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
242
+ paddingLeft={current() ? 1 : 3}
243
+ paddingRight={3}
244
+ gap={1}
245
+ >
246
+ <Option
247
+ title={option.title}
248
+ footer={option.footer}
249
+ description={option.description !== category ? option.description : undefined}
250
+ active={active()}
251
+ current={current()}
252
+ />
253
+ </box>
254
+ )
255
+ }}
256
+ </For>
257
+ </>
258
+ )}
259
+ </For>
260
+ </scrollbox>
261
+ <Show when={keybinds().length} fallback={<box flexShrink={0} />}>
262
+ <box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
263
+ <For each={keybinds()}>
264
+ {(item) => (
265
+ <text>
266
+ <span style={{ fg: theme.text }}>
267
+ <b>{item.title}</b>{" "}
268
+ </span>
269
+ <span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
270
+ </text>
271
+ )}
272
+ </For>
273
+ </box>
274
+ </Show>
275
+ </box>
276
+ )
277
+ }
278
+
279
+ function Option(props: {
280
+ title: string
281
+ description?: string
282
+ active?: boolean
283
+ current?: boolean
284
+ footer?: JSX.Element | string
285
+ onMouseOver?: () => void
286
+ }) {
287
+ const { theme } = useTheme()
288
+ const fg = selectedForeground(theme)
289
+
290
+ return (
291
+ <>
292
+ <Show when={props.current}>
293
+ <text flexShrink={0} fg={props.active ? fg : props.current ? theme.primary : theme.text} marginRight={0.5}>
294
+
295
+ </text>
296
+ </Show>
297
+ <text
298
+ flexGrow={1}
299
+ fg={props.active ? fg : props.current ? theme.primary : theme.text}
300
+ attributes={props.active ? TextAttributes.BOLD : undefined}
301
+ overflow="hidden"
302
+ wrapMode="word"
303
+ paddingLeft={3}
304
+ >
305
+ {Locale.truncate(props.title, 62)}
306
+ <Show when={props.description}>
307
+ <span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
308
+ </Show>
309
+ </text>
310
+ <Show when={props.footer}>
311
+ <box flexShrink={0}>
312
+ <text fg={props.active ? fg : theme.textMuted}>{props.footer}</text>
313
+ </box>
314
+ </Show>
315
+ </>
316
+ )
317
+ }