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,586 @@
1
+ import { useSync } from "@tui/context/sync"
2
+ import { createMemo, createEffect, createSignal, For, Show, Switch, Match } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import { useTheme } from "../../context/theme"
5
+ import { useToast } from "../../ui/toast"
6
+ import { useLocal } from "../../context/local"
7
+ import { Locale } from "@/util/locale"
8
+ import path from "path"
9
+ import type { AssistantMessage } from "@opencode-ai/sdk/v2"
10
+ import { Installation } from "@/installation"
11
+ import { useKeybind } from "../../context/keybind"
12
+ import { useDirectory } from "../../context/directory"
13
+
14
+ // Threshold for low cache hit rate warning
15
+ const LOW_CACHE_HIT_THRESHOLD = 40
16
+ const CONSECUTIVE_LOW_COUNT = 3
17
+
18
+ // Convert percentage (0-100) to block character (8 levels)
19
+ function percentToBar(percent: number): string {
20
+ const blocks = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
21
+ const index = Math.round((percent / 100) * 8)
22
+ return blocks[Math.min(8, Math.max(0, index))]
23
+ }
24
+
25
+ // Row with label and horizontal bar for rate limits
26
+ function RateLimitRow(props: {
27
+ remaining: number
28
+ limit: number
29
+ window: string
30
+ }) {
31
+ const { theme } = useTheme()
32
+
33
+ const percentRemaining = createMemo(() =>
34
+ props.limit > 0 ? (props.remaining / props.limit) * 100 : 100
35
+ )
36
+
37
+ // Bar width fits in column
38
+ const barWidth = 10
39
+ const filledBlocks = createMemo(() => Math.round((percentRemaining() / 100) * barWidth))
40
+ const progressBar = createMemo(() => {
41
+ const filled = filledBlocks()
42
+ const empty = barWidth - filled
43
+ return "█".repeat(filled) + "░".repeat(empty)
44
+ })
45
+
46
+ const barColor = createMemo(() => {
47
+ const percent = percentRemaining()
48
+ if (percent >= 50) return theme.success
49
+ if (percent >= 20) return theme.warning
50
+ return theme.error
51
+ })
52
+
53
+ // Window label
54
+ const windowLabel = () => {
55
+ if (props.window === "minute") return "min"
56
+ if (props.window === "hour") return "hour"
57
+ if (props.window === "day") return "day"
58
+ return props.window
59
+ }
60
+
61
+ return (
62
+ <box flexDirection="row" gap={1}>
63
+ <text fg={theme.textMuted} width={4}>{windowLabel()}</text>
64
+ <text>
65
+ <span style={{ fg: barColor() }}>{progressBar()}</span>
66
+ </text>
67
+ </box>
68
+ )
69
+ }
70
+
71
+ // Visual representation of cache hit rate
72
+ function CacheVisual(props: {
73
+ hitRate: number
74
+ cachedTokens: number
75
+ promptTokens: number
76
+ recentRates: number[] // Last 10 message hit rates
77
+ }) {
78
+ const { theme } = useTheme()
79
+
80
+ // Progress bar using block characters
81
+ const barWidth = 20
82
+ const filledBlocks = createMemo(() => Math.round((props.hitRate / 100) * barWidth))
83
+ const progressBar = createMemo(() => {
84
+ const filled = filledBlocks()
85
+ const empty = barWidth - filled
86
+ return "█".repeat(filled) + "░".repeat(empty)
87
+ })
88
+
89
+ // Pie/wheel indicator using circle segments
90
+ const pieIndicator = createMemo(() => {
91
+ const rate = props.hitRate
92
+ if (rate >= 87.5) return "●" // Full
93
+ if (rate >= 62.5) return "◕" // 3/4
94
+ if (rate >= 37.5) return "◑" // Half
95
+ if (rate >= 12.5) return "◔" // 1/4
96
+ return "○" // Empty
97
+ })
98
+
99
+ // Minesweeper-style face indicator
100
+ const faceIndicator = createMemo(() => {
101
+ const rate = props.hitRate
102
+ if (rate >= 70) return "😊" // Happy - good cache
103
+ if (rate >= 40) return "😐" // Neutral - okay cache
104
+ return "😟" // Worried - bad cache
105
+ })
106
+
107
+ // Color based on hit rate (gradient from red to green)
108
+ const rateColor = createMemo(() => {
109
+ const rate = props.hitRate
110
+ if (rate >= 70) return theme.success
111
+ if (rate >= 40) return theme.warning
112
+ return theme.error
113
+ })
114
+
115
+ // Get color for a rate
116
+ const getRateColor = (rate: number) => {
117
+ if (rate >= 70) return theme.success
118
+ if (rate >= 40) return theme.warning
119
+ return theme.error
120
+ }
121
+
122
+ // Last 10 rates as a memo for proper reactivity
123
+ const recentRates = createMemo(() => {
124
+ const rates = props.recentRates || []
125
+ return rates.slice(-10)
126
+ })
127
+
128
+ // Pre-compute the sparkline to avoid For reactivity issues
129
+ const sparkline = createMemo(() => {
130
+ return recentRates().map((rate, i) => ({
131
+ key: i,
132
+ rate,
133
+ char: percentToBar(rate),
134
+ color: getRateColor(rate),
135
+ }))
136
+ })
137
+
138
+ return (
139
+ <>
140
+ {/* Wheel indicator with percentage and face */}
141
+ <box flexDirection="row" gap={1}>
142
+ <text style={{ fg: rateColor() }}>{pieIndicator()}</text>
143
+ <text fg={theme.textMuted}>
144
+ {props.hitRate.toFixed(1)}% hit rate
145
+ </text>
146
+ <text>{faceIndicator()}</text>
147
+ </box>
148
+ {/* Progress bar with sparkline bar chart */}
149
+ <box flexDirection="row" gap={1}>
150
+ <text>
151
+ <span style={{ fg: rateColor() }}>{progressBar()}</span>
152
+ </text>
153
+ <text>
154
+ <For each={sparkline()}>
155
+ {(item) => (
156
+ <span style={{ fg: item.color }}>{item.char}</span>
157
+ )}
158
+ </For>
159
+ </text>
160
+ </box>
161
+ {/* Token counts */}
162
+ <text fg={theme.textMuted}>
163
+ {props.cachedTokens.toLocaleString()} / {props.promptTokens.toLocaleString()} tokens
164
+ </text>
165
+ </>
166
+ )
167
+ }
168
+
169
+ export function Sidebar(props: { sessionID: string }) {
170
+ const sync = useSync()
171
+ const { theme } = useTheme()
172
+ const toast = useToast()
173
+ const local = useLocal()
174
+ const session = createMemo(() => sync.session.get(props.sessionID)!)
175
+ const diff = createMemo(() => sync.data.session_diff[props.sessionID] ?? [])
176
+ const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
177
+ const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
178
+
179
+ // Track whether we've shown the low cache warning for this session
180
+ const [hasShownCacheWarning, setHasShownCacheWarning] = createSignal(false)
181
+ const [lastMessageCount, setLastMessageCount] = createSignal(0)
182
+
183
+ const [expanded, setExpanded] = createStore({
184
+ mcp: true,
185
+ diff: true,
186
+ todo: true,
187
+ lsp: true,
188
+ })
189
+
190
+ // Sort MCP servers alphabetically for consistent display order
191
+ const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b)))
192
+
193
+ const usage = createMemo(() => {
194
+ const now = Date.now()
195
+ const assistants = messages().filter((m) => m.role === "assistant")
196
+ const total = assistants.length
197
+ const countWithin = (ms: number) =>
198
+ assistants.filter((m) => {
199
+ const t = m.time?.completed ?? m.time?.created ?? 0
200
+ return now - t <= ms
201
+ }).length
202
+ return {
203
+ total,
204
+ min1: countWithin(60_000),
205
+ hour1: countWithin(60 * 60_000),
206
+ day1: countWithin(24 * 60 * 60_000),
207
+ }
208
+ })
209
+
210
+ const context = createMemo(() => {
211
+ const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
212
+ if (!last) return
213
+ const total =
214
+ last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
215
+ const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
216
+ return {
217
+ tokens: total.toLocaleString(),
218
+ percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
219
+ }
220
+ })
221
+
222
+ // Get rate limit info from sync store (populated via SSE events)
223
+ const rateLimitInfo = createMemo(() => {
224
+ const currentModel = local.model.current()
225
+ if (!currentModel?.providerID) return undefined
226
+ return sync.data.ratelimit[currentModel.providerID]
227
+ })
228
+
229
+
230
+ const cacheStats = createMemo(() => {
231
+ const assistants = messages().filter((m) => m.role === "assistant") as AssistantMessage[]
232
+ let totalCachedTokens = 0
233
+ let totalPromptTokens = 0
234
+ for (const msg of assistants) {
235
+ // Total prompt = input + cached (input may be non-cached portion only)
236
+ const cached = msg.tokens.cache.read
237
+ const total = msg.tokens.input + cached
238
+ totalCachedTokens += cached
239
+ totalPromptTokens += total
240
+ }
241
+ const hitRate = totalPromptTokens > 0 ? (totalCachedTokens / totalPromptTokens) * 100 : 0
242
+ return {
243
+ promptTokens: totalPromptTokens,
244
+ cachedTokens: totalCachedTokens,
245
+ hitRate: hitRate.toFixed(1),
246
+ }
247
+ })
248
+
249
+ // Calculate per-message cache hit rates for completed assistant messages
250
+ const perMessageCacheRates = createMemo(() => {
251
+ const assistants = messages().filter(
252
+ (m) => m.role === "assistant" && m.time.completed
253
+ ) as AssistantMessage[]
254
+ return assistants.map((msg) => {
255
+ const cached = msg.tokens.cache.read
256
+ const total = msg.tokens.input + cached
257
+ return total > 0 ? (cached / total) * 100 : 0
258
+ })
259
+ })
260
+
261
+ // Monitor for consecutive low cache hit rates
262
+ createEffect(() => {
263
+ const rates = perMessageCacheRates()
264
+ const currentCount = rates.length
265
+
266
+ // Only check when we have new completed messages
267
+ if (currentCount <= lastMessageCount()) {
268
+ return
269
+ }
270
+ setLastMessageCount(currentCount)
271
+
272
+ if (rates.length < CONSECUTIVE_LOW_COUNT) {
273
+ return
274
+ }
275
+
276
+ const lastNRates = rates.slice(-CONSECUTIVE_LOW_COUNT)
277
+ const allBelowThreshold = lastNRates.every((rate) => rate < LOW_CACHE_HIT_THRESHOLD)
278
+
279
+ if (allBelowThreshold && !hasShownCacheWarning()) {
280
+ setHasShownCacheWarning(true)
281
+ toast.show({
282
+ variant: "warning",
283
+ title: "Low Cache Hit Rate",
284
+ message: `Cache hit rate has been below ${LOW_CACHE_HIT_THRESHOLD}% for the last ${CONSECUTIVE_LOW_COUNT} requests. This may increase costs and latency.`,
285
+ duration: 8000,
286
+ })
287
+ }
288
+
289
+ if (!allBelowThreshold && hasShownCacheWarning()) {
290
+ const lastNAboveThreshold = lastNRates.every((rate) => rate >= LOW_CACHE_HIT_THRESHOLD)
291
+ if (lastNAboveThreshold) {
292
+ setHasShownCacheWarning(false)
293
+ }
294
+ }
295
+ })
296
+
297
+ const keybind = useKeybind()
298
+ const directory = useDirectory()
299
+
300
+ const hasProviders = createMemo(() =>
301
+ sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
302
+ )
303
+
304
+ return (
305
+ <Show when={session()}>
306
+ <box
307
+ backgroundColor={theme.backgroundPanel}
308
+ width={42}
309
+ paddingTop={1}
310
+ paddingBottom={1}
311
+ paddingLeft={2}
312
+ paddingRight={2}
313
+ >
314
+ <scrollbox flexGrow={1}>
315
+ <box flexShrink={0} gap={1} paddingRight={1}>
316
+ <box>
317
+ <text fg={theme.text}>
318
+ <b>{session().title}</b>
319
+ </text>
320
+ <Show when={session().share?.url}>
321
+ <text fg={theme.textMuted}>{session().share!.url}</text>
322
+ </Show>
323
+ </box>
324
+ <box>
325
+ <text fg={theme.text}>
326
+ <b>Context</b>
327
+ </text>
328
+ <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
329
+ <text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
330
+ <text fg={theme.textMuted}>
331
+ Requests: {usage().total} (1m {usage().min1} / 1h {usage().hour1} / 24h {usage().day1})
332
+ </text>
333
+ </box>
334
+ <Show when={cacheStats().promptTokens > 0}>
335
+ <box>
336
+ <text fg={theme.text}>
337
+ <b>Cache</b>
338
+ </text>
339
+ <CacheVisual
340
+ hitRate={parseFloat(cacheStats().hitRate)}
341
+ cachedTokens={cacheStats().cachedTokens}
342
+ promptTokens={cacheStats().promptTokens}
343
+ recentRates={perMessageCacheRates()}
344
+ />
345
+ </box>
346
+ </Show>
347
+ <Show when={rateLimitInfo()}>
348
+ <box>
349
+ <text fg={theme.text}>
350
+ <b>Rate Limits</b>
351
+ </text>
352
+ <box flexDirection="row" gap={2}>
353
+ {/* Tokens column */}
354
+ <box flexGrow={1} gap={1}>
355
+ <text fg={theme.textMuted}>Tokens</text>
356
+ <For each={rateLimitInfo()?.tokenLimits || []}>
357
+ {(windowInfo) => (
358
+ <RateLimitRow
359
+ remaining={windowInfo.remaining}
360
+ limit={windowInfo.limit}
361
+ window={windowInfo.window}
362
+ />
363
+ )}
364
+ </For>
365
+ </box>
366
+ {/* Requests column */}
367
+ <box flexGrow={1} gap={1}>
368
+ <text fg={theme.textMuted}>Requests</text>
369
+ <For each={rateLimitInfo()?.requestLimits || []}>
370
+ {(windowInfo) => (
371
+ <RateLimitRow
372
+ remaining={windowInfo.remaining}
373
+ limit={windowInfo.limit}
374
+ window={windowInfo.window}
375
+ />
376
+ )}
377
+ </For>
378
+ </box>
379
+ </box>
380
+ </box>
381
+ </Show>
382
+ <Show when={mcpEntries().length > 0}>
383
+ <box>
384
+ <box
385
+ flexDirection="row"
386
+ gap={1}
387
+ onMouseDown={() => mcpEntries().length > 2 && setExpanded("mcp", !expanded.mcp)}
388
+ >
389
+ <Show when={mcpEntries().length > 2}>
390
+ <text fg={theme.text}>{expanded.mcp ? "▼" : "▶"}</text>
391
+ </Show>
392
+ <text fg={theme.text}>
393
+ <b>MCP</b>
394
+ </text>
395
+ </box>
396
+ <Show when={mcpEntries().length <= 2 || expanded.mcp}>
397
+ <For each={mcpEntries()}>
398
+ {([key, item]) => (
399
+ <box flexDirection="row" gap={1}>
400
+ <text
401
+ flexShrink={0}
402
+ style={{
403
+ fg: (
404
+ {
405
+ connected: theme.success,
406
+ failed: theme.error,
407
+ disabled: theme.textMuted,
408
+ needs_auth: theme.warning,
409
+ needs_client_registration: theme.error,
410
+ } as Record<string, typeof theme.success>
411
+ )[item.status],
412
+ }}
413
+ >
414
+
415
+ </text>
416
+ <text fg={theme.text} wrapMode="word">
417
+ {key}{" "}
418
+ <span style={{ fg: theme.textMuted }}>
419
+ <Switch fallback={item.status}>
420
+ <Match when={item.status === "connected"}>Connected</Match>
421
+ <Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
422
+ <Match when={item.status === "disabled"}>Disabled</Match>
423
+ <Match when={(item.status as string) === "needs_auth"}>Needs auth</Match>
424
+ <Match when={(item.status as string) === "needs_client_registration"}>
425
+ Needs client ID
426
+ </Match>
427
+ </Switch>
428
+ </span>
429
+ </text>
430
+ </box>
431
+ )}
432
+ </For>
433
+ </Show>
434
+ </box>
435
+ </Show>
436
+ <box>
437
+ <box
438
+ flexDirection="row"
439
+ gap={1}
440
+ onMouseDown={() => sync.data.lsp.length > 2 && setExpanded("lsp", !expanded.lsp)}
441
+ >
442
+ <Show when={sync.data.lsp.length > 2}>
443
+ <text fg={theme.text}>{expanded.lsp ? "▼" : "▶"}</text>
444
+ </Show>
445
+ <text fg={theme.text}>
446
+ <b>LSP</b>
447
+ </text>
448
+ </box>
449
+ <Show when={sync.data.lsp.length <= 2 || expanded.lsp}>
450
+ <Show when={sync.data.lsp.length === 0}>
451
+ <text fg={theme.textMuted}>LSPs will activate as files are read</text>
452
+ </Show>
453
+ <For each={sync.data.lsp}>
454
+ {(item) => (
455
+ <box flexDirection="row" gap={1}>
456
+ <text
457
+ flexShrink={0}
458
+ style={{
459
+ fg: {
460
+ connected: theme.success,
461
+ error: theme.error,
462
+ }[item.status],
463
+ }}
464
+ >
465
+
466
+ </text>
467
+ <text fg={theme.textMuted}>
468
+ {item.id} {item.root}
469
+ </text>
470
+ </box>
471
+ )}
472
+ </For>
473
+ </Show>
474
+ </box>
475
+ <Show when={todo().length > 0 && todo().some((t) => t.status !== "completed")}>
476
+ <box>
477
+ <box
478
+ flexDirection="row"
479
+ gap={1}
480
+ onMouseDown={() => todo().length > 2 && setExpanded("todo", !expanded.todo)}
481
+ >
482
+ <Show when={todo().length > 2}>
483
+ <text fg={theme.text}>{expanded.todo ? "▼" : "▶"}</text>
484
+ </Show>
485
+ <text fg={theme.text}>
486
+ <b>Todo</b>
487
+ </text>
488
+ </box>
489
+ <Show when={todo().length <= 2 || expanded.todo}>
490
+ <For each={todo()}>
491
+ {(todo) => (
492
+ <text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
493
+ [{todo.status === "completed" ? "✓" : " "}] {todo.content}
494
+ </text>
495
+ )}
496
+ </For>
497
+ </Show>
498
+ </box>
499
+ </Show>
500
+ <Show when={diff().length > 0}>
501
+ <box>
502
+ <box
503
+ flexDirection="row"
504
+ gap={1}
505
+ onMouseDown={() => diff().length > 2 && setExpanded("diff", !expanded.diff)}
506
+ >
507
+ <Show when={diff().length > 2}>
508
+ <text fg={theme.text}>{expanded.diff ? "▼" : "▶"}</text>
509
+ </Show>
510
+ <text fg={theme.text}>
511
+ <b>Modified Files</b>
512
+ </text>
513
+ </box>
514
+ <Show when={diff().length <= 2 || expanded.diff}>
515
+ <For each={diff() || []}>
516
+ {(item) => {
517
+ const file = createMemo(() => {
518
+ const splits = item.file.split(path.sep).filter(Boolean)
519
+ const last = splits.at(-1)!
520
+ const rest = splits.slice(0, -1).join(path.sep)
521
+ if (!rest) return last
522
+ return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
523
+ })
524
+ return (
525
+ <box flexDirection="row" gap={1} justifyContent="space-between">
526
+ <text fg={theme.textMuted} wrapMode="char">
527
+ {file()}
528
+ </text>
529
+ <box flexDirection="row" gap={1} flexShrink={0}>
530
+ <Show when={item.additions}>
531
+ <text fg={theme.diffAdded}>+{item.additions}</text>
532
+ </Show>
533
+ <Show when={item.deletions}>
534
+ <text fg={theme.diffRemoved}>-{item.deletions}</text>
535
+ </Show>
536
+ </box>
537
+ </box>
538
+ )
539
+ }}
540
+ </For>
541
+ </Show>
542
+ </box>
543
+ </Show>
544
+ </box>
545
+ </scrollbox>
546
+
547
+ <box flexShrink={0} gap={1} paddingTop={1}>
548
+ <Show when={!hasProviders()}>
549
+ <box
550
+ backgroundColor={theme.backgroundElement}
551
+ paddingTop={1}
552
+ paddingBottom={1}
553
+ paddingLeft={2}
554
+ paddingRight={2}
555
+ flexDirection="row"
556
+ gap={1}
557
+ >
558
+ <text flexShrink={0}>⬖</text>
559
+ <box flexGrow={1} gap={1}>
560
+ <text>
561
+ <b>Getting started</b>
562
+ </text>
563
+ <text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
564
+ <text fg={theme.textMuted}>
565
+ Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
566
+ </text>
567
+ <box flexDirection="row" gap={1} justifyContent="space-between">
568
+ <text>Connect provider</text>
569
+ <text fg={theme.textMuted}>/connect</text>
570
+ </box>
571
+ </box>
572
+ </box>
573
+ </Show>
574
+ <text fg={theme.text}>{directory()}</text>
575
+ <text fg={theme.textMuted}>
576
+ <span style={{ fg: theme.success }}>•</span> <b>Open</b>
577
+ <span style={{ fg: theme.text }}>
578
+ <b>Code</b>
579
+ </span>{" "}
580
+ <span>{Installation.VERSION}</span>
581
+ </text>
582
+ </box>
583
+ </box>
584
+ </Show>
585
+ )
586
+ }
@@ -0,0 +1,60 @@
1
+ import { cmd } from "@/cli/cmd/cmd"
2
+ import { Instance } from "@/project/instance"
3
+ import path from "path"
4
+ import { Server } from "@/server/server"
5
+ import { upgrade } from "@/cli/upgrade"
6
+
7
+ export const TuiSpawnCommand = cmd({
8
+ command: "spawn [project]",
9
+ builder: (yargs) =>
10
+ yargs
11
+ .positional("project", {
12
+ type: "string",
13
+ describe: "path to start opencode in",
14
+ })
15
+ .option("port", {
16
+ type: "number",
17
+ describe: "port to listen on",
18
+ default: 0,
19
+ })
20
+ .option("hostname", {
21
+ type: "string",
22
+ describe: "hostname to listen on",
23
+ default: "127.0.0.1",
24
+ }),
25
+ handler: async (args) => {
26
+ upgrade()
27
+ const server = Server.listen({
28
+ port: args.port,
29
+ hostname: "127.0.0.1",
30
+ })
31
+ const bin = process.execPath
32
+ const cmd = []
33
+ let cwd = process.cwd()
34
+ if (bin.endsWith("bun")) {
35
+ cmd.push(
36
+ process.execPath,
37
+ "run",
38
+ "--conditions",
39
+ "browser",
40
+ new URL("../../../index.ts", import.meta.url).pathname,
41
+ )
42
+ cwd = new URL("../../../../", import.meta.url).pathname
43
+ } else cmd.push(process.execPath)
44
+ cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
45
+ const proc = Bun.spawn({
46
+ cmd,
47
+ cwd,
48
+ stdout: "inherit",
49
+ stderr: "inherit",
50
+ stdin: "inherit",
51
+ env: {
52
+ ...process.env,
53
+ BUN_OPTIONS: "",
54
+ },
55
+ })
56
+ await proc.exited
57
+ await Instance.disposeAll()
58
+ await server.stop(true)
59
+ },
60
+ })