cerebras-cli 1.0.5 → 1.0.138

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 (322) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +10 -0
  3. package/README.md +5 -3
  4. package/bin/{opencode.cjs → opencode} +4 -4
  5. package/bunfig.toml +4 -0
  6. package/package.json +89 -32
  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/snake_game.py +111 -0
  13. package/src/acp/README.md +164 -0
  14. package/src/acp/agent.ts +812 -0
  15. package/src/acp/session.ts +70 -0
  16. package/src/acp/types.ts +22 -0
  17. package/src/agent/agent.ts +310 -0
  18. package/src/agent/generate.txt +75 -0
  19. package/src/auth/index.ts +70 -0
  20. package/src/bun/index.ts +152 -0
  21. package/src/bus/global.ts +10 -0
  22. package/src/bus/index.ts +142 -0
  23. package/src/cli/bootstrap.ts +17 -0
  24. package/src/cli/cmd/acp.ts +88 -0
  25. package/src/cli/cmd/agent.ts +165 -0
  26. package/src/cli/cmd/auth.ts +369 -0
  27. package/src/cli/cmd/cmd.ts +7 -0
  28. package/src/cli/cmd/debug/config.ts +15 -0
  29. package/src/cli/cmd/debug/file.ts +91 -0
  30. package/src/cli/cmd/debug/index.ts +43 -0
  31. package/src/cli/cmd/debug/lsp.ts +47 -0
  32. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  33. package/src/cli/cmd/debug/scrap.ts +15 -0
  34. package/src/cli/cmd/debug/skill.ts +36 -0
  35. package/src/cli/cmd/debug/snapshot.ts +48 -0
  36. package/src/cli/cmd/export.ts +88 -0
  37. package/src/cli/cmd/generate.ts +38 -0
  38. package/src/cli/cmd/github.ts +1200 -0
  39. package/src/cli/cmd/import.ts +98 -0
  40. package/src/cli/cmd/mcp.ts +400 -0
  41. package/src/cli/cmd/models.ts +77 -0
  42. package/src/cli/cmd/pr.ts +112 -0
  43. package/src/cli/cmd/run.ts +342 -0
  44. package/src/cli/cmd/serve.ts +31 -0
  45. package/src/cli/cmd/session.ts +106 -0
  46. package/src/cli/cmd/stats.ts +298 -0
  47. package/src/cli/cmd/tui/app.tsx +833 -0
  48. package/src/cli/cmd/tui/attach.ts +25 -0
  49. package/src/cli/cmd/tui/component/border.tsx +21 -0
  50. package/src/cli/cmd/tui/component/cerebras-onboarding.tsx +225 -0
  51. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  52. package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
  53. package/src/cli/cmd/tui/component/dialog-feedback.tsx +160 -0
  54. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  55. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  56. package/src/cli/cmd/tui/component/dialog-notification.tsx +78 -0
  57. package/src/cli/cmd/tui/component/dialog-provider.tsx +222 -0
  58. package/src/cli/cmd/tui/component/dialog-session-list.tsx +97 -0
  59. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  60. package/src/cli/cmd/tui/component/dialog-status.tsx +114 -0
  61. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  62. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  63. package/src/cli/cmd/tui/component/logo.tsx +43 -0
  64. package/src/cli/cmd/tui/component/notification-banner.tsx +58 -0
  65. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +530 -0
  66. package/src/cli/cmd/tui/component/prompt/history.tsx +107 -0
  67. package/src/cli/cmd/tui/component/prompt/index.tsx +931 -0
  68. package/src/cli/cmd/tui/component/quickstart-onboarding.tsx +116 -0
  69. package/src/cli/cmd/tui/context/args.tsx +14 -0
  70. package/src/cli/cmd/tui/context/directory.ts +12 -0
  71. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  72. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  73. package/src/cli/cmd/tui/context/keybind.tsx +111 -0
  74. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  75. package/src/cli/cmd/tui/context/local.tsx +338 -0
  76. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  77. package/src/cli/cmd/tui/context/route.tsx +45 -0
  78. package/src/cli/cmd/tui/context/sdk.tsx +75 -0
  79. package/src/cli/cmd/tui/context/sync.tsx +374 -0
  80. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  81. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  82. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  83. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  84. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  85. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  86. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  87. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  88. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  89. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  90. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  91. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  92. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  93. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  94. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  95. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  96. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  97. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  98. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  99. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  100. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  101. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  102. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  103. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  104. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  105. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  106. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  107. package/src/cli/cmd/tui/context/theme.tsx +1077 -0
  108. package/src/cli/cmd/tui/event.ts +39 -0
  109. package/src/cli/cmd/tui/routes/home.tsx +150 -0
  110. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +93 -0
  111. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +37 -0
  112. package/src/cli/cmd/tui/routes/session/footer.tsx +76 -0
  113. package/src/cli/cmd/tui/routes/session/header.tsx +181 -0
  114. package/src/cli/cmd/tui/routes/session/index.tsx +1695 -0
  115. package/src/cli/cmd/tui/routes/session/sidebar.tsx +686 -0
  116. package/src/cli/cmd/tui/spawn.ts +60 -0
  117. package/src/cli/cmd/tui/thread.ts +120 -0
  118. package/src/cli/cmd/tui/ui/dialog-alert.tsx +55 -0
  119. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +81 -0
  120. package/src/cli/cmd/tui/ui/dialog-help.tsx +36 -0
  121. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +75 -0
  122. package/src/cli/cmd/tui/ui/dialog-select.tsx +317 -0
  123. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  124. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  125. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  126. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  127. package/src/cli/cmd/tui/util/editor.ts +32 -0
  128. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  129. package/src/cli/cmd/tui/worker.ts +63 -0
  130. package/src/cli/cmd/uninstall.ts +344 -0
  131. package/src/cli/cmd/upgrade.ts +67 -0
  132. package/src/cli/cmd/web.ts +84 -0
  133. package/src/cli/error.ts +55 -0
  134. package/src/cli/ui.ts +84 -0
  135. package/src/cli/upgrade.ts +25 -0
  136. package/src/command/index.ts +79 -0
  137. package/src/command/template/initialize.txt +10 -0
  138. package/src/command/template/review.txt +73 -0
  139. package/src/config/config.ts +886 -0
  140. package/src/config/markdown.ts +41 -0
  141. package/src/env/index.ts +26 -0
  142. package/src/file/fzf.ts +124 -0
  143. package/src/file/ignore.ts +83 -0
  144. package/src/file/index.ts +326 -0
  145. package/src/file/ripgrep.ts +391 -0
  146. package/src/file/time.ts +38 -0
  147. package/src/file/watcher.ts +89 -0
  148. package/src/flag/flag.ts +29 -0
  149. package/src/format/formatter.ts +277 -0
  150. package/src/format/index.ts +137 -0
  151. package/src/global/index.ts +52 -0
  152. package/src/id/id.ts +73 -0
  153. package/src/ide/index.ts +75 -0
  154. package/src/index.ts +158 -0
  155. package/src/installation/index.ts +194 -0
  156. package/src/lsp/client.ts +215 -0
  157. package/src/lsp/index.ts +370 -0
  158. package/src/lsp/language.ts +111 -0
  159. package/src/lsp/server.ts +1327 -0
  160. package/src/mcp/auth.ts +82 -0
  161. package/src/mcp/index.ts +576 -0
  162. package/src/mcp/oauth-callback.ts +203 -0
  163. package/src/mcp/oauth-provider.ts +132 -0
  164. package/src/notification/index.ts +101 -0
  165. package/src/patch/index.ts +622 -0
  166. package/src/permission/index.ts +198 -0
  167. package/src/plugin/index.ts +95 -0
  168. package/src/project/bootstrap.ts +31 -0
  169. package/src/project/instance.ts +68 -0
  170. package/src/project/project.ts +133 -0
  171. package/src/project/state.ts +65 -0
  172. package/src/project/vcs.ts +77 -0
  173. package/src/provider/auth.ts +143 -0
  174. package/src/provider/models-macro.ts +11 -0
  175. package/src/provider/models.ts +93 -0
  176. package/src/provider/provider.ts +1005 -0
  177. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  178. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  179. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  180. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  181. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +27 -0
  182. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  183. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  184. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  185. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  190. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  191. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  192. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  193. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  194. package/src/provider/transform.ts +406 -0
  195. package/src/pty/index.ts +226 -0
  196. package/src/ratelimit/index.ts +185 -0
  197. package/src/server/error.ts +36 -0
  198. package/src/server/project.ts +50 -0
  199. package/src/server/server.ts +2463 -0
  200. package/src/server/tui.ts +71 -0
  201. package/src/session/compaction.ts +257 -0
  202. package/src/session/index.ts +470 -0
  203. package/src/session/message-v2.ts +641 -0
  204. package/src/session/message.ts +189 -0
  205. package/src/session/processor.ts +448 -0
  206. package/src/session/prompt/anthropic-20250930.txt +166 -0
  207. package/src/session/prompt/anthropic.txt +105 -0
  208. package/src/session/prompt/anthropic_spoof.txt +1 -0
  209. package/src/session/prompt/beast.txt +147 -0
  210. package/src/session/prompt/build-switch.txt +5 -0
  211. package/src/session/prompt/codex.txt +318 -0
  212. package/src/session/prompt/compaction.txt +12 -0
  213. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  214. package/src/session/prompt/gemini.txt +155 -0
  215. package/src/session/prompt/max-steps.txt +16 -0
  216. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  217. package/src/session/prompt/plan.txt +26 -0
  218. package/src/session/prompt/polaris.txt +107 -0
  219. package/src/session/prompt/qwen.txt +109 -0
  220. package/src/session/prompt/summarize.txt +4 -0
  221. package/src/session/prompt/title.txt +36 -0
  222. package/src/session/prompt.ts +1541 -0
  223. package/src/session/retry.ts +82 -0
  224. package/src/session/revert.ts +108 -0
  225. package/src/session/status.ts +75 -0
  226. package/src/session/summary.ts +203 -0
  227. package/src/session/system.ts +148 -0
  228. package/src/session/todo.ts +36 -0
  229. package/src/share/share-next.ts +195 -0
  230. package/src/share/share.ts +87 -0
  231. package/src/skill/index.ts +2 -0
  232. package/src/skill/skill.ts +138 -0
  233. package/src/snapshot/index.ts +197 -0
  234. package/src/storage/storage.ts +226 -0
  235. package/src/telemetry/index.ts +247 -0
  236. package/src/tool/bash.ts +365 -0
  237. package/src/tool/bash.txt +128 -0
  238. package/src/tool/batch.ts +173 -0
  239. package/src/tool/batch.txt +28 -0
  240. package/src/tool/codesearch.ts +138 -0
  241. package/src/tool/codesearch.txt +12 -0
  242. package/src/tool/edit.ts +674 -0
  243. package/src/tool/edit.txt +10 -0
  244. package/src/tool/glob.ts +65 -0
  245. package/src/tool/glob.txt +6 -0
  246. package/src/tool/grep.ts +120 -0
  247. package/src/tool/grep.txt +8 -0
  248. package/src/tool/invalid.ts +17 -0
  249. package/src/tool/ls.ts +110 -0
  250. package/src/tool/ls.txt +1 -0
  251. package/src/tool/lsp-diagnostics.ts +26 -0
  252. package/src/tool/lsp-diagnostics.txt +1 -0
  253. package/src/tool/lsp-hover.ts +31 -0
  254. package/src/tool/lsp-hover.txt +1 -0
  255. package/src/tool/multiedit.ts +46 -0
  256. package/src/tool/multiedit.txt +41 -0
  257. package/src/tool/patch.ts +233 -0
  258. package/src/tool/patch.txt +1 -0
  259. package/src/tool/read.ts +217 -0
  260. package/src/tool/read.txt +12 -0
  261. package/src/tool/registry.ts +150 -0
  262. package/src/tool/skill.ts +85 -0
  263. package/src/tool/task.ts +135 -0
  264. package/src/tool/task.txt +60 -0
  265. package/src/tool/todo.ts +39 -0
  266. package/src/tool/todoread.txt +14 -0
  267. package/src/tool/todowrite.txt +167 -0
  268. package/src/tool/tool.ts +66 -0
  269. package/src/tool/webfetch.ts +187 -0
  270. package/src/tool/webfetch.txt +14 -0
  271. package/src/tool/websearch.ts +150 -0
  272. package/src/tool/websearch.txt +11 -0
  273. package/src/tool/write.ts +99 -0
  274. package/src/tool/write.txt +8 -0
  275. package/src/types/shims.d.ts +3 -0
  276. package/src/util/color.ts +19 -0
  277. package/src/util/context.ts +25 -0
  278. package/src/util/defer.ts +12 -0
  279. package/src/util/eventloop.ts +20 -0
  280. package/src/util/filesystem.ts +69 -0
  281. package/src/util/fn.ts +11 -0
  282. package/src/util/iife.ts +3 -0
  283. package/src/util/keybind.ts +79 -0
  284. package/src/util/lazy.ts +11 -0
  285. package/src/util/locale.ts +81 -0
  286. package/src/util/lock.ts +98 -0
  287. package/src/util/log.ts +177 -0
  288. package/src/util/queue.ts +32 -0
  289. package/src/util/rpc.ts +42 -0
  290. package/src/util/scrap.ts +10 -0
  291. package/src/util/signal.ts +12 -0
  292. package/src/util/timeout.ts +14 -0
  293. package/src/util/token.ts +7 -0
  294. package/src/util/wildcard.ts +54 -0
  295. package/sst-env.d.ts +9 -0
  296. package/test/bun.test.ts +53 -0
  297. package/test/config/agent-color.test.ts +66 -0
  298. package/test/config/config.test.ts +503 -0
  299. package/test/config/markdown.test.ts +89 -0
  300. package/test/file/ignore.test.ts +10 -0
  301. package/test/fixture/fixture.ts +28 -0
  302. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  303. package/test/ide/ide.test.ts +82 -0
  304. package/test/keybind.test.ts +317 -0
  305. package/test/lsp/client.test.ts +95 -0
  306. package/test/patch/patch.test.ts +348 -0
  307. package/test/preload.ts +38 -0
  308. package/test/project/project.test.ts +42 -0
  309. package/test/provider/provider.test.ts +1809 -0
  310. package/test/provider/transform.test.ts +305 -0
  311. package/test/session/retry.test.ts +61 -0
  312. package/test/session/session.test.ts +71 -0
  313. package/test/snapshot/snapshot.test.ts +939 -0
  314. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  315. package/test/tool/bash.test.ts +55 -0
  316. package/test/tool/patch.test.ts +259 -0
  317. package/test/util/iife.test.ts +36 -0
  318. package/test/util/lazy.test.ts +50 -0
  319. package/test/util/timeout.test.ts +21 -0
  320. package/test/util/wildcard.test.ts +55 -0
  321. package/tsconfig.json +17 -0
  322. package/cerebras-cli-1.0.0.tgz +0 -0
@@ -0,0 +1,686 @@
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
+ // Clamp remaining to be >= 0 and <= limit to handle edge cases
34
+ // (e.g., first message hitting rate limits where remaining could be 0 or negative)
35
+ const safeRemaining = createMemo(() =>
36
+ Math.max(0, Math.min(props.remaining ?? 0, props.limit ?? 0))
37
+ )
38
+
39
+ // If limit is 0 or missing, show 0% (empty bar) not 100% (full bar)
40
+ const percentRemaining = createMemo(() =>
41
+ props.limit > 0 ? (safeRemaining() / props.limit) * 100 : 0
42
+ )
43
+
44
+ // Bar width fits in column
45
+ const barWidth = 10
46
+ const filledBlocks = createMemo(() => Math.round((percentRemaining() / 100) * barWidth))
47
+ const progressBar = createMemo(() => {
48
+ const filled = Math.max(0, Math.min(barWidth, filledBlocks()))
49
+ const empty = barWidth - filled
50
+ return "█".repeat(filled) + "░".repeat(empty)
51
+ })
52
+
53
+ const barColor = createMemo(() => {
54
+ const percent = percentRemaining()
55
+ if (percent >= 50) return theme.success
56
+ if (percent >= 20) return theme.warning
57
+ return theme.error
58
+ })
59
+
60
+ // Window label
61
+ const windowLabel = () => {
62
+ if (props.window === "minute") return "min"
63
+ if (props.window === "hour") return "hour"
64
+ if (props.window === "day") return "day"
65
+ return props.window
66
+ }
67
+
68
+ return (
69
+ <box flexDirection="row" gap={1}>
70
+ <text fg={theme.textMuted} width={4}>{windowLabel()}</text>
71
+ <text>
72
+ <span style={{ fg: barColor() }}>{progressBar()}</span>
73
+ </text>
74
+ </box>
75
+ )
76
+ }
77
+
78
+ function ContextProgressBar(props: {
79
+ used: number
80
+ limit: number
81
+ }) {
82
+ const { theme } = useTheme()
83
+
84
+ const percentUsed = createMemo(() =>
85
+ props.limit > 0 ? Math.min(100, (props.used / props.limit) * 100) : 0
86
+ )
87
+
88
+ const percentRemaining = createMemo(() => 100 - percentUsed())
89
+
90
+ const barWidth = 20
91
+ const filledBlocks = createMemo(() => Math.round((percentUsed() / 100) * barWidth))
92
+ const progressBar = createMemo(() => {
93
+ const filled = filledBlocks()
94
+ const empty = barWidth - filled
95
+ return "█".repeat(filled) + "░".repeat(empty)
96
+ })
97
+
98
+ // Color based on remaining (green = plenty left, red = almost full)
99
+ const barColor = createMemo(() => {
100
+ const remaining = percentRemaining()
101
+ if (remaining >= 50) return theme.success
102
+ if (remaining >= 20) return theme.warning
103
+ return theme.error
104
+ })
105
+
106
+ const formatTokens = (tokens: number): string => {
107
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`
108
+ if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`
109
+ return tokens.toLocaleString()
110
+ }
111
+
112
+ return (
113
+ <box>
114
+ <box flexDirection="row" gap={1}>
115
+ <text>
116
+ <span style={{ fg: barColor() }}>{progressBar()}</span>
117
+ </text>
118
+ <text fg={theme.textMuted}>{percentUsed().toFixed(0)}%</text>
119
+ </box>
120
+ <text fg={theme.textMuted}>
121
+ {formatTokens(props.used)} / {formatTokens(props.limit)}
122
+ </text>
123
+ </box>
124
+ )
125
+ }
126
+
127
+ // Visual representation of cache hit rate
128
+ function CacheVisual(props: {
129
+ hitRate: number
130
+ cachedTokens: number
131
+ promptTokens: number
132
+ recentRates: number[] // Last 10 message hit rates
133
+ }) {
134
+ const { theme } = useTheme()
135
+
136
+ // Progress bar using block characters
137
+ const barWidth = 20
138
+ const filledBlocks = createMemo(() => Math.round((props.hitRate / 100) * barWidth))
139
+ const progressBar = createMemo(() => {
140
+ const filled = filledBlocks()
141
+ const empty = barWidth - filled
142
+ return "█".repeat(filled) + "░".repeat(empty)
143
+ })
144
+
145
+ // Pie/wheel indicator using circle segments
146
+ const pieIndicator = createMemo(() => {
147
+ const rate = props.hitRate
148
+ if (rate >= 87.5) return "●" // Full
149
+ if (rate >= 62.5) return "◕" // 3/4
150
+ if (rate >= 37.5) return "◑" // Half
151
+ if (rate >= 12.5) return "◔" // 1/4
152
+ return "○" // Empty
153
+ })
154
+
155
+ // Minesweeper-style face indicator
156
+ const faceIndicator = createMemo(() => {
157
+ const rate = props.hitRate
158
+ if (rate >= 70) return "😊" // Happy - good cache
159
+ if (rate >= 40) return "😐" // Neutral - okay cache
160
+ return "😟" // Worried - bad cache
161
+ })
162
+
163
+ // Color based on hit rate (gradient from red to green)
164
+ const rateColor = createMemo(() => {
165
+ const rate = props.hitRate
166
+ if (rate >= 70) return theme.success
167
+ if (rate >= 40) return theme.warning
168
+ return theme.error
169
+ })
170
+
171
+ // Get color for a rate
172
+ const getRateColor = (rate: number) => {
173
+ if (rate >= 70) return theme.success
174
+ if (rate >= 40) return theme.warning
175
+ return theme.error
176
+ }
177
+
178
+ // Last 10 rates as a memo for proper reactivity
179
+ const recentRates = createMemo(() => {
180
+ const rates = props.recentRates || []
181
+ return rates.slice(-10)
182
+ })
183
+
184
+ // Pre-compute the sparkline to avoid For reactivity issues
185
+ const sparkline = createMemo(() => {
186
+ return recentRates().map((rate, i) => ({
187
+ key: i,
188
+ rate,
189
+ char: percentToBar(rate),
190
+ color: getRateColor(rate),
191
+ }))
192
+ })
193
+
194
+ return (
195
+ <>
196
+ {/* Wheel indicator with percentage and face */}
197
+ <box flexDirection="row" gap={1}>
198
+ <text style={{ fg: rateColor() }}>{pieIndicator()}</text>
199
+ <text fg={theme.textMuted}>
200
+ {props.hitRate.toFixed(1)}% hit rate
201
+ </text>
202
+ <text>{faceIndicator()}</text>
203
+ </box>
204
+ {/* Progress bar with sparkline bar chart */}
205
+ <box flexDirection="row" gap={1}>
206
+ <text>
207
+ <span style={{ fg: rateColor() }}>{progressBar()}</span>
208
+ </text>
209
+ <text>
210
+ <For each={sparkline()}>
211
+ {(item) => (
212
+ <span style={{ fg: item.color }}>{item.char}</span>
213
+ )}
214
+ </For>
215
+ </text>
216
+ </box>
217
+ {/* Token counts */}
218
+ <text fg={theme.textMuted}>
219
+ {props.cachedTokens.toLocaleString()} / {props.promptTokens.toLocaleString()} tokens
220
+ </text>
221
+ </>
222
+ )
223
+ }
224
+
225
+ export function Sidebar(props: { sessionID: string }) {
226
+ const sync = useSync()
227
+ const { theme } = useTheme()
228
+ const toast = useToast()
229
+ const local = useLocal()
230
+ const session = createMemo(() => sync.session.get(props.sessionID)!)
231
+ const diff = createMemo(() => sync.data.session_diff[props.sessionID] ?? [])
232
+ const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
233
+ const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
234
+
235
+ // Track whether we've shown the low cache warning for this session
236
+ const [hasShownCacheWarning, setHasShownCacheWarning] = createSignal(false)
237
+ const [lastMessageCount, setLastMessageCount] = createSignal(0)
238
+
239
+ const [expanded, setExpanded] = createStore({
240
+ mcp: true,
241
+ diff: true,
242
+ todo: true,
243
+ lsp: true,
244
+ })
245
+
246
+ // Sort MCP servers alphabetically for consistent display order
247
+ const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b)))
248
+
249
+ const usage = createMemo(() => {
250
+ const now = Date.now()
251
+ const assistants = messages().filter((m) => m.role === "assistant")
252
+ const total = assistants.length
253
+ const countWithin = (ms: number) =>
254
+ assistants.filter((m) => {
255
+ const t = m.time?.completed ?? m.time?.created ?? 0
256
+ return now - t <= ms
257
+ }).length
258
+ return {
259
+ total,
260
+ min1: countWithin(60_000),
261
+ hour1: countWithin(60 * 60_000),
262
+ day1: countWithin(24 * 60 * 60_000),
263
+ }
264
+ })
265
+
266
+ const context = createMemo(() => {
267
+ const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
268
+ if (!last) return
269
+ // Context sent = input tokens + cached tokens (what was actually sent to the API)
270
+ const used = last.tokens.input + last.tokens.cache.read
271
+ const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
272
+ // Default to 128K (131072) for Cerebras GLM-4.7
273
+ const limit = model?.limit.context || 131072
274
+ const percentage = Math.min(100, Math.round((used / limit) * 100))
275
+ return {
276
+ used,
277
+ limit,
278
+ percentage,
279
+ }
280
+ })
281
+
282
+ // Get rate limit info from sync store (populated via SSE events)
283
+ const rateLimitInfo = createMemo(() => {
284
+ const currentModel = local.model.current()
285
+ if (!currentModel?.providerID) return undefined
286
+ return sync.data.ratelimit[currentModel.providerID]
287
+ })
288
+
289
+ // Determine API key tier based on rate limits
290
+ // Free: 10 RPM, 60K TPM, 1M TPD
291
+ // Pro: 50 RPM, 1M TPM, 24M TPD
292
+ // Max: 120 RPM, 1.5M TPM, 120M TPD
293
+ // PayGo: Higher than Max
294
+ const apiTier = createMemo(() => {
295
+ const info = rateLimitInfo()
296
+ if (!info) return null
297
+
298
+ // Find minute limits
299
+ const requestsPerMin = info.requestLimits?.find(w => w.window === "minute")?.limit
300
+ const tokensPerMin = info.tokenLimits?.find(w => w.window === "minute")?.limit
301
+ const tokensPerDay = info.tokenLimits?.find(w => w.window === "day")?.limit
302
+
303
+ if (requestsPerMin === undefined) return null
304
+
305
+ if (requestsPerMin <= 10) {
306
+ return { name: "Free Tier", color: theme.textMuted }
307
+ } else if (requestsPerMin <= 50) {
308
+ return { name: "Pro Plan", color: theme.success }
309
+ } else if (requestsPerMin <= 120) {
310
+ return { name: "Max Plan", color: theme.warning }
311
+ } else {
312
+ return { name: "PayGo", color: theme.accent }
313
+ }
314
+ })
315
+
316
+
317
+ const cacheStats = createMemo(() => {
318
+ const assistants = messages().filter((m) => m.role === "assistant") as AssistantMessage[]
319
+ let totalCachedTokens = 0
320
+ let totalPromptTokens = 0
321
+ for (const msg of assistants) {
322
+ // Total prompt = input + cached (input may be non-cached portion only)
323
+ const cached = msg.tokens.cache.read
324
+ const total = msg.tokens.input + cached
325
+ totalCachedTokens += cached
326
+ totalPromptTokens += total
327
+ }
328
+ const hitRate = totalPromptTokens > 0 ? (totalCachedTokens / totalPromptTokens) * 100 : 0
329
+ return {
330
+ promptTokens: totalPromptTokens,
331
+ cachedTokens: totalCachedTokens,
332
+ hitRate: hitRate.toFixed(1),
333
+ }
334
+ })
335
+
336
+ // Calculate per-message cache hit rates for completed assistant messages
337
+ const perMessageCacheRates = createMemo(() => {
338
+ const assistants = messages().filter(
339
+ (m) => m.role === "assistant" && m.time.completed
340
+ ) as AssistantMessage[]
341
+ return assistants.map((msg) => {
342
+ const cached = msg.tokens.cache.read
343
+ const total = msg.tokens.input + cached
344
+ return total > 0 ? (cached / total) * 100 : 0
345
+ })
346
+ })
347
+
348
+ // Monitor for consecutive low cache hit rates
349
+ createEffect(() => {
350
+ const rates = perMessageCacheRates()
351
+ const currentCount = rates.length
352
+
353
+ // Only check when we have new completed messages
354
+ if (currentCount <= lastMessageCount()) {
355
+ return
356
+ }
357
+ setLastMessageCount(currentCount)
358
+
359
+ if (rates.length < CONSECUTIVE_LOW_COUNT) {
360
+ return
361
+ }
362
+
363
+ const lastNRates = rates.slice(-CONSECUTIVE_LOW_COUNT)
364
+ const allBelowThreshold = lastNRates.every((rate) => rate < LOW_CACHE_HIT_THRESHOLD)
365
+
366
+ if (allBelowThreshold && !hasShownCacheWarning()) {
367
+ setHasShownCacheWarning(true)
368
+ toast.show({
369
+ variant: "warning",
370
+ title: "Low Cache Hit Rate",
371
+ message: `Cache hit rate has been below ${LOW_CACHE_HIT_THRESHOLD}% for the last ${CONSECUTIVE_LOW_COUNT} requests. This may increase costs and latency.`,
372
+ duration: 8000,
373
+ })
374
+ }
375
+
376
+ if (!allBelowThreshold && hasShownCacheWarning()) {
377
+ const lastNAboveThreshold = lastNRates.every((rate) => rate >= LOW_CACHE_HIT_THRESHOLD)
378
+ if (lastNAboveThreshold) {
379
+ setHasShownCacheWarning(false)
380
+ }
381
+ }
382
+ })
383
+
384
+ const keybind = useKeybind()
385
+ const directory = useDirectory()
386
+
387
+ const hasProviders = createMemo(() =>
388
+ sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
389
+ )
390
+
391
+ return (
392
+ <Show when={session()}>
393
+ <box
394
+ backgroundColor={theme.backgroundPanel}
395
+ width={42}
396
+ paddingTop={1}
397
+ paddingBottom={1}
398
+ paddingLeft={2}
399
+ paddingRight={2}
400
+ >
401
+ <scrollbox flexGrow={1}>
402
+ <box flexShrink={0} gap={1} paddingRight={1}>
403
+ <box>
404
+ <text fg={theme.text}>
405
+ <b>{session().title}</b>
406
+ </text>
407
+ <Show when={session().share?.url}>
408
+ <text fg={theme.textMuted}>{session().share!.url}</text>
409
+ </Show>
410
+ </box>
411
+ <box>
412
+ <text fg={theme.text}>
413
+ <b>Context</b>
414
+ </text>
415
+ <Show when={context()} fallback={
416
+ <text fg={theme.textMuted}>No requests yet</text>
417
+ }>
418
+ <ContextProgressBar
419
+ used={context()!.used}
420
+ limit={context()!.limit}
421
+ />
422
+ </Show>
423
+ <text fg={theme.textMuted}>
424
+ Requests: {usage().total} (1m {usage().min1} / 1h {usage().hour1} / 24h {usage().day1})
425
+ </text>
426
+ </box>
427
+ <Show when={cacheStats().promptTokens > 0}>
428
+ <box>
429
+ <text fg={theme.text}>
430
+ <b>Cache</b>
431
+ </text>
432
+ <CacheVisual
433
+ hitRate={parseFloat(cacheStats().hitRate)}
434
+ cachedTokens={cacheStats().cachedTokens}
435
+ promptTokens={cacheStats().promptTokens}
436
+ recentRates={perMessageCacheRates()}
437
+ />
438
+ </box>
439
+ </Show>
440
+ <Show when={rateLimitInfo()}>
441
+ <box>
442
+ <box flexDirection="row" gap={1}>
443
+ <text fg={theme.text}>
444
+ <b>Rate Limits</b>
445
+ </text>
446
+ <Show when={apiTier()}>
447
+ <text style={{ fg: apiTier()!.color }}>
448
+ [{apiTier()!.name}]
449
+ </text>
450
+ </Show>
451
+ </box>
452
+ <box flexDirection="row" gap={2}>
453
+ {/* Tokens column */}
454
+ <box flexGrow={1} gap={1}>
455
+ <text fg={theme.textMuted}>Tokens</text>
456
+ <For each={rateLimitInfo()?.tokenLimits || []}>
457
+ {(windowInfo) => (
458
+ <RateLimitRow
459
+ remaining={windowInfo.remaining}
460
+ limit={windowInfo.limit}
461
+ window={windowInfo.window}
462
+ />
463
+ )}
464
+ </For>
465
+ </box>
466
+ {/* Requests column */}
467
+ <box flexGrow={1} gap={1}>
468
+ <text fg={theme.textMuted}>Requests</text>
469
+ <For each={rateLimitInfo()?.requestLimits || []}>
470
+ {(windowInfo) => (
471
+ <RateLimitRow
472
+ remaining={windowInfo.remaining}
473
+ limit={windowInfo.limit}
474
+ window={windowInfo.window}
475
+ />
476
+ )}
477
+ </For>
478
+ </box>
479
+ </box>
480
+ </box>
481
+ </Show>
482
+ <Show when={mcpEntries().length > 0}>
483
+ <box>
484
+ <box
485
+ flexDirection="row"
486
+ gap={1}
487
+ onMouseDown={() => mcpEntries().length > 2 && setExpanded("mcp", !expanded.mcp)}
488
+ >
489
+ <Show when={mcpEntries().length > 2}>
490
+ <text fg={theme.text}>{expanded.mcp ? "▼" : "▶"}</text>
491
+ </Show>
492
+ <text fg={theme.text}>
493
+ <b>MCP</b>
494
+ </text>
495
+ </box>
496
+ <Show when={mcpEntries().length <= 2 || expanded.mcp}>
497
+ <For each={mcpEntries()}>
498
+ {([key, item]) => (
499
+ <box flexDirection="row" gap={1}>
500
+ <text
501
+ flexShrink={0}
502
+ style={{
503
+ fg: (
504
+ {
505
+ connected: theme.success,
506
+ failed: theme.error,
507
+ disabled: theme.textMuted,
508
+ needs_auth: theme.warning,
509
+ needs_client_registration: theme.error,
510
+ } as Record<string, typeof theme.success>
511
+ )[item.status],
512
+ }}
513
+ >
514
+
515
+ </text>
516
+ <text fg={theme.text} wrapMode="word">
517
+ {key}{" "}
518
+ <span style={{ fg: theme.textMuted }}>
519
+ <Switch fallback={item.status}>
520
+ <Match when={item.status === "connected"}>Connected</Match>
521
+ <Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
522
+ <Match when={item.status === "disabled"}>Disabled</Match>
523
+ <Match when={(item.status as string) === "needs_auth"}>Needs auth</Match>
524
+ <Match when={(item.status as string) === "needs_client_registration"}>
525
+ Needs client ID
526
+ </Match>
527
+ </Switch>
528
+ </span>
529
+ </text>
530
+ </box>
531
+ )}
532
+ </For>
533
+ </Show>
534
+ </box>
535
+ </Show>
536
+ <box>
537
+ <box
538
+ flexDirection="row"
539
+ gap={1}
540
+ onMouseDown={() => sync.data.lsp.length > 2 && setExpanded("lsp", !expanded.lsp)}
541
+ >
542
+ <Show when={sync.data.lsp.length > 2}>
543
+ <text fg={theme.text}>{expanded.lsp ? "▼" : "▶"}</text>
544
+ </Show>
545
+ <text fg={theme.text}>
546
+ <b>LSP</b>
547
+ </text>
548
+ </box>
549
+ <Show when={sync.data.lsp.length <= 2 || expanded.lsp}>
550
+ <Show when={sync.data.lsp.length === 0}>
551
+ <text fg={theme.textMuted}>LSPs will activate as files are read</text>
552
+ </Show>
553
+ <For each={sync.data.lsp}>
554
+ {(item) => (
555
+ <box flexDirection="row" gap={1}>
556
+ <text
557
+ flexShrink={0}
558
+ style={{
559
+ fg: {
560
+ connected: theme.success,
561
+ error: theme.error,
562
+ }[item.status],
563
+ }}
564
+ >
565
+
566
+ </text>
567
+ <text fg={theme.textMuted}>
568
+ {item.id} {item.root}
569
+ </text>
570
+ </box>
571
+ )}
572
+ </For>
573
+ </Show>
574
+ </box>
575
+ <Show when={todo().length > 0 && todo().some((t) => t.status !== "completed")}>
576
+ <box>
577
+ <box
578
+ flexDirection="row"
579
+ gap={1}
580
+ onMouseDown={() => todo().length > 2 && setExpanded("todo", !expanded.todo)}
581
+ >
582
+ <Show when={todo().length > 2}>
583
+ <text fg={theme.text}>{expanded.todo ? "▼" : "▶"}</text>
584
+ </Show>
585
+ <text fg={theme.text}>
586
+ <b>Todo</b>
587
+ </text>
588
+ </box>
589
+ <Show when={todo().length <= 2 || expanded.todo}>
590
+ <For each={todo()}>
591
+ {(todo) => (
592
+ <text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
593
+ [{todo.status === "completed" ? "✓" : " "}] {todo.content}
594
+ </text>
595
+ )}
596
+ </For>
597
+ </Show>
598
+ </box>
599
+ </Show>
600
+ <Show when={diff().length > 0}>
601
+ <box>
602
+ <box
603
+ flexDirection="row"
604
+ gap={1}
605
+ onMouseDown={() => diff().length > 2 && setExpanded("diff", !expanded.diff)}
606
+ >
607
+ <Show when={diff().length > 2}>
608
+ <text fg={theme.text}>{expanded.diff ? "▼" : "▶"}</text>
609
+ </Show>
610
+ <text fg={theme.text}>
611
+ <b>Modified Files</b>
612
+ </text>
613
+ </box>
614
+ <Show when={diff().length <= 2 || expanded.diff}>
615
+ <For each={diff() || []}>
616
+ {(item) => {
617
+ const file = createMemo(() => {
618
+ const splits = item.file.split(path.sep).filter(Boolean)
619
+ const last = splits.at(-1)!
620
+ const rest = splits.slice(0, -1).join(path.sep)
621
+ if (!rest) return last
622
+ return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
623
+ })
624
+ return (
625
+ <box flexDirection="row" gap={1} justifyContent="space-between">
626
+ <text fg={theme.textMuted} wrapMode="char">
627
+ {file()}
628
+ </text>
629
+ <box flexDirection="row" gap={1} flexShrink={0}>
630
+ <Show when={item.additions}>
631
+ <text fg={theme.diffAdded}>+{item.additions}</text>
632
+ </Show>
633
+ <Show when={item.deletions}>
634
+ <text fg={theme.diffRemoved}>-{item.deletions}</text>
635
+ </Show>
636
+ </box>
637
+ </box>
638
+ )
639
+ }}
640
+ </For>
641
+ </Show>
642
+ </box>
643
+ </Show>
644
+ </box>
645
+ </scrollbox>
646
+
647
+ <box flexShrink={0} gap={1} paddingTop={1}>
648
+ <Show when={!hasProviders()}>
649
+ <box
650
+ backgroundColor={theme.backgroundElement}
651
+ paddingTop={1}
652
+ paddingBottom={1}
653
+ paddingLeft={2}
654
+ paddingRight={2}
655
+ flexDirection="row"
656
+ gap={1}
657
+ >
658
+ <text flexShrink={0}>⬖</text>
659
+ <box flexGrow={1} gap={1}>
660
+ <text>
661
+ <b>Getting started</b>
662
+ </text>
663
+ <text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
664
+ <text fg={theme.textMuted}>
665
+ Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
666
+ </text>
667
+ <box flexDirection="row" gap={1} justifyContent="space-between">
668
+ <text>Connect provider</text>
669
+ <text fg={theme.textMuted}>/connect</text>
670
+ </box>
671
+ </box>
672
+ </box>
673
+ </Show>
674
+ <text fg={theme.text}>{directory()}</text>
675
+ <text fg={theme.textMuted}>
676
+ <span style={{ fg: theme.success }}>•</span> <b>Open</b>
677
+ <span style={{ fg: theme.text }}>
678
+ <b>Code</b>
679
+ </span>{" "}
680
+ <span>{Installation.VERSION}</span>
681
+ </text>
682
+ </box>
683
+ </box>
684
+ </Show>
685
+ )
686
+ }