@stonerzju/opencode 1.2.17 → 1.2.19

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 (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,510 +0,0 @@
1
- import { afterEach, expect, test } from "bun:test"
2
- import path from "path"
3
- import fs from "fs/promises"
4
- import { tmpdir } from "../fixture/fixture"
5
- import { Instance } from "../../src/project/instance"
6
- import { TuiConfig } from "../../src/config/tui"
7
- import { Global } from "../../src/global"
8
- import { Filesystem } from "../../src/util/filesystem"
9
-
10
- const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
11
-
12
- afterEach(async () => {
13
- delete process.env.OPENCODE_CONFIG
14
- delete process.env.OPENCODE_TUI_CONFIG
15
- await fs.rm(path.join(Global.Path.config, "tui.json"), { force: true }).catch(() => {})
16
- await fs.rm(path.join(Global.Path.config, "tui.jsonc"), { force: true }).catch(() => {})
17
- await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
18
- })
19
-
20
- test("loads tui config with the same precedence order as server config paths", async () => {
21
- await using tmp = await tmpdir({
22
- init: async (dir) => {
23
- await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ theme: "global" }, null, 2))
24
- await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project" }, null, 2))
25
- await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
26
- await Bun.write(
27
- path.join(dir, ".opencode", "tui.json"),
28
- JSON.stringify({ theme: "local", diff_style: "stacked" }, null, 2),
29
- )
30
- },
31
- })
32
-
33
- await Instance.provide({
34
- directory: tmp.path,
35
- fn: async () => {
36
- const config = await TuiConfig.get()
37
- expect(config.theme).toBe("local")
38
- expect(config.diff_style).toBe("stacked")
39
- },
40
- })
41
- })
42
-
43
- test("migrates tui-specific keys from opencode.json when tui.json does not exist", async () => {
44
- await using tmp = await tmpdir({
45
- init: async (dir) => {
46
- await Bun.write(
47
- path.join(dir, "opencode.json"),
48
- JSON.stringify(
49
- {
50
- theme: "migrated-theme",
51
- tui: { scroll_speed: 5 },
52
- keybinds: { app_exit: "ctrl+q" },
53
- },
54
- null,
55
- 2,
56
- ),
57
- )
58
- },
59
- })
60
-
61
- await Instance.provide({
62
- directory: tmp.path,
63
- fn: async () => {
64
- const config = await TuiConfig.get()
65
- expect(config.theme).toBe("migrated-theme")
66
- expect(config.scroll_speed).toBe(5)
67
- expect(config.keybinds?.app_exit).toBe("ctrl+q")
68
- const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
69
- expect(JSON.parse(text)).toMatchObject({
70
- theme: "migrated-theme",
71
- scroll_speed: 5,
72
- })
73
- const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
74
- expect(server.theme).toBeUndefined()
75
- expect(server.keybinds).toBeUndefined()
76
- expect(server.tui).toBeUndefined()
77
- expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(true)
78
- expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
79
- },
80
- })
81
- })
82
-
83
- test("migrates project legacy tui keys even when global tui.json already exists", async () => {
84
- await using tmp = await tmpdir({
85
- init: async (dir) => {
86
- await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ theme: "global" }, null, 2))
87
- await Bun.write(
88
- path.join(dir, "opencode.json"),
89
- JSON.stringify(
90
- {
91
- theme: "project-migrated",
92
- tui: { scroll_speed: 2 },
93
- },
94
- null,
95
- 2,
96
- ),
97
- )
98
- },
99
- })
100
-
101
- await Instance.provide({
102
- directory: tmp.path,
103
- fn: async () => {
104
- const config = await TuiConfig.get()
105
- expect(config.theme).toBe("project-migrated")
106
- expect(config.scroll_speed).toBe(2)
107
- expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
108
-
109
- const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
110
- expect(server.theme).toBeUndefined()
111
- expect(server.tui).toBeUndefined()
112
- },
113
- })
114
- })
115
-
116
- test("drops unknown legacy tui keys during migration", async () => {
117
- await using tmp = await tmpdir({
118
- init: async (dir) => {
119
- await Bun.write(
120
- path.join(dir, "opencode.json"),
121
- JSON.stringify(
122
- {
123
- theme: "migrated-theme",
124
- tui: { scroll_speed: 2, foo: 1 },
125
- },
126
- null,
127
- 2,
128
- ),
129
- )
130
- },
131
- })
132
-
133
- await Instance.provide({
134
- directory: tmp.path,
135
- fn: async () => {
136
- const config = await TuiConfig.get()
137
- expect(config.theme).toBe("migrated-theme")
138
- expect(config.scroll_speed).toBe(2)
139
-
140
- const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
141
- const migrated = JSON.parse(text)
142
- expect(migrated.scroll_speed).toBe(2)
143
- expect(migrated.foo).toBeUndefined()
144
- },
145
- })
146
- })
147
-
148
- test("skips migration when opencode.jsonc is syntactically invalid", async () => {
149
- await using tmp = await tmpdir({
150
- init: async (dir) => {
151
- await Bun.write(
152
- path.join(dir, "opencode.jsonc"),
153
- `{
154
- "theme": "broken-theme",
155
- "tui": { "scroll_speed": 2 }
156
- "username": "still-broken"
157
- }`,
158
- )
159
- },
160
- })
161
-
162
- await Instance.provide({
163
- directory: tmp.path,
164
- fn: async () => {
165
- const config = await TuiConfig.get()
166
- expect(config.theme).toBeUndefined()
167
- expect(config.scroll_speed).toBeUndefined()
168
- expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(false)
169
- expect(await Filesystem.exists(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))).toBe(false)
170
- const source = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc"))
171
- expect(source).toContain('"theme": "broken-theme"')
172
- expect(source).toContain('"tui": { "scroll_speed": 2 }')
173
- },
174
- })
175
- })
176
-
177
- test("skips migration when tui.json already exists", async () => {
178
- await using tmp = await tmpdir({
179
- init: async (dir) => {
180
- await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "legacy" }, null, 2))
181
- await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2))
182
- },
183
- })
184
-
185
- await Instance.provide({
186
- directory: tmp.path,
187
- fn: async () => {
188
- const config = await TuiConfig.get()
189
- expect(config.diff_style).toBe("stacked")
190
- expect(config.theme).toBeUndefined()
191
-
192
- const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
193
- expect(server.theme).toBe("legacy")
194
- expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(false)
195
- },
196
- })
197
- })
198
-
199
- test("continues loading tui config when legacy source cannot be stripped", async () => {
200
- await using tmp = await tmpdir({
201
- init: async (dir) => {
202
- await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "readonly-theme" }, null, 2))
203
- },
204
- })
205
-
206
- const source = path.join(tmp.path, "opencode.json")
207
- await fs.chmod(source, 0o444)
208
-
209
- try {
210
- await Instance.provide({
211
- directory: tmp.path,
212
- fn: async () => {
213
- const config = await TuiConfig.get()
214
- expect(config.theme).toBe("readonly-theme")
215
- expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
216
-
217
- const server = JSON.parse(await Filesystem.readText(source))
218
- expect(server.theme).toBe("readonly-theme")
219
- },
220
- })
221
- } finally {
222
- await fs.chmod(source, 0o644)
223
- }
224
- })
225
-
226
- test("migration backup preserves JSONC comments", async () => {
227
- await using tmp = await tmpdir({
228
- init: async (dir) => {
229
- await Bun.write(
230
- path.join(dir, "opencode.jsonc"),
231
- `{
232
- // top-level comment
233
- "theme": "jsonc-theme",
234
- "tui": {
235
- // nested comment
236
- "scroll_speed": 1.5
237
- }
238
- }`,
239
- )
240
- },
241
- })
242
-
243
- await Instance.provide({
244
- directory: tmp.path,
245
- fn: async () => {
246
- await TuiConfig.get()
247
- const backup = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))
248
- expect(backup).toContain("// top-level comment")
249
- expect(backup).toContain("// nested comment")
250
- expect(backup).toContain('"theme": "jsonc-theme"')
251
- expect(backup).toContain('"scroll_speed": 1.5')
252
- },
253
- })
254
- })
255
-
256
- test("migrates legacy tui keys across multiple opencode.json levels", async () => {
257
- await using tmp = await tmpdir({
258
- init: async (dir) => {
259
- const nested = path.join(dir, "apps", "client")
260
- await fs.mkdir(nested, { recursive: true })
261
- await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ theme: "root-theme" }, null, 2))
262
- await Bun.write(path.join(nested, "opencode.json"), JSON.stringify({ theme: "nested-theme" }, null, 2))
263
- },
264
- })
265
-
266
- await Instance.provide({
267
- directory: path.join(tmp.path, "apps", "client"),
268
- fn: async () => {
269
- const config = await TuiConfig.get()
270
- expect(config.theme).toBe("nested-theme")
271
- expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
272
- expect(await Filesystem.exists(path.join(tmp.path, "apps", "client", "tui.json"))).toBe(true)
273
- },
274
- })
275
- })
276
-
277
- test("flattens nested tui key inside tui.json", async () => {
278
- await using tmp = await tmpdir({
279
- init: async (dir) => {
280
- await Bun.write(
281
- path.join(dir, "tui.json"),
282
- JSON.stringify({
283
- theme: "outer",
284
- tui: { scroll_speed: 3, diff_style: "stacked" },
285
- }),
286
- )
287
- },
288
- })
289
-
290
- await Instance.provide({
291
- directory: tmp.path,
292
- fn: async () => {
293
- const config = await TuiConfig.get()
294
- expect(config.scroll_speed).toBe(3)
295
- expect(config.diff_style).toBe("stacked")
296
- // top-level keys take precedence over nested tui keys
297
- expect(config.theme).toBe("outer")
298
- },
299
- })
300
- })
301
-
302
- test("top-level keys in tui.json take precedence over nested tui key", async () => {
303
- await using tmp = await tmpdir({
304
- init: async (dir) => {
305
- await Bun.write(
306
- path.join(dir, "tui.json"),
307
- JSON.stringify({
308
- diff_style: "auto",
309
- tui: { diff_style: "stacked", scroll_speed: 2 },
310
- }),
311
- )
312
- },
313
- })
314
-
315
- await Instance.provide({
316
- directory: tmp.path,
317
- fn: async () => {
318
- const config = await TuiConfig.get()
319
- expect(config.diff_style).toBe("auto")
320
- expect(config.scroll_speed).toBe(2)
321
- },
322
- })
323
- })
324
-
325
- test("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE_CONFIG)", async () => {
326
- await using tmp = await tmpdir({
327
- init: async (dir) => {
328
- await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project", diff_style: "auto" }))
329
- const custom = path.join(dir, "custom-tui.json")
330
- await Bun.write(custom, JSON.stringify({ theme: "custom", diff_style: "stacked" }))
331
- process.env.OPENCODE_TUI_CONFIG = custom
332
- },
333
- })
334
-
335
- await Instance.provide({
336
- directory: tmp.path,
337
- fn: async () => {
338
- const config = await TuiConfig.get()
339
- // project tui.json overrides the custom path, same as server config precedence
340
- expect(config.theme).toBe("project")
341
- // project also set diff_style, so that wins
342
- expect(config.diff_style).toBe("auto")
343
- },
344
- })
345
- })
346
-
347
- test("merges keybind overrides across precedence layers", async () => {
348
- await using tmp = await tmpdir({
349
- init: async (dir) => {
350
- await Bun.write(path.join(Global.Path.config, "tui.json"), JSON.stringify({ keybinds: { app_exit: "ctrl+q" } }))
351
- await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { theme_list: "ctrl+k" } }))
352
- },
353
- })
354
-
355
- await Instance.provide({
356
- directory: tmp.path,
357
- fn: async () => {
358
- const config = await TuiConfig.get()
359
- expect(config.keybinds?.app_exit).toBe("ctrl+q")
360
- expect(config.keybinds?.theme_list).toBe("ctrl+k")
361
- },
362
- })
363
- })
364
-
365
- test("OPENCODE_TUI_CONFIG provides settings when no project config exists", async () => {
366
- await using tmp = await tmpdir({
367
- init: async (dir) => {
368
- const custom = path.join(dir, "custom-tui.json")
369
- await Bun.write(custom, JSON.stringify({ theme: "from-env", diff_style: "stacked" }))
370
- process.env.OPENCODE_TUI_CONFIG = custom
371
- },
372
- })
373
-
374
- await Instance.provide({
375
- directory: tmp.path,
376
- fn: async () => {
377
- const config = await TuiConfig.get()
378
- expect(config.theme).toBe("from-env")
379
- expect(config.diff_style).toBe("stacked")
380
- },
381
- })
382
- })
383
-
384
- test("does not derive tui path from OPENCODE_CONFIG", async () => {
385
- await using tmp = await tmpdir({
386
- init: async (dir) => {
387
- const customDir = path.join(dir, "custom")
388
- await fs.mkdir(customDir, { recursive: true })
389
- await Bun.write(path.join(customDir, "opencode.json"), JSON.stringify({ model: "test/model" }))
390
- await Bun.write(path.join(customDir, "tui.json"), JSON.stringify({ theme: "should-not-load" }))
391
- process.env.OPENCODE_CONFIG = path.join(customDir, "opencode.json")
392
- },
393
- })
394
-
395
- await Instance.provide({
396
- directory: tmp.path,
397
- fn: async () => {
398
- const config = await TuiConfig.get()
399
- expect(config.theme).toBeUndefined()
400
- },
401
- })
402
- })
403
-
404
- test("applies env and file substitutions in tui.json", async () => {
405
- const original = process.env.TUI_THEME_TEST
406
- process.env.TUI_THEME_TEST = "env-theme"
407
- try {
408
- await using tmp = await tmpdir({
409
- init: async (dir) => {
410
- await Bun.write(path.join(dir, "keybind.txt"), "ctrl+q")
411
- await Bun.write(
412
- path.join(dir, "tui.json"),
413
- JSON.stringify({
414
- theme: "{env:TUI_THEME_TEST}",
415
- keybinds: { app_exit: "{file:keybind.txt}" },
416
- }),
417
- )
418
- },
419
- })
420
-
421
- await Instance.provide({
422
- directory: tmp.path,
423
- fn: async () => {
424
- const config = await TuiConfig.get()
425
- expect(config.theme).toBe("env-theme")
426
- expect(config.keybinds?.app_exit).toBe("ctrl+q")
427
- },
428
- })
429
- } finally {
430
- if (original === undefined) delete process.env.TUI_THEME_TEST
431
- else process.env.TUI_THEME_TEST = original
432
- }
433
- })
434
-
435
- test("applies file substitutions when first identical token is in a commented line", async () => {
436
- await using tmp = await tmpdir({
437
- init: async (dir) => {
438
- await Bun.write(path.join(dir, "theme.txt"), "resolved-theme")
439
- await Bun.write(
440
- path.join(dir, "tui.jsonc"),
441
- `{
442
- // "theme": "{file:theme.txt}",
443
- "theme": "{file:theme.txt}"
444
- }`,
445
- )
446
- },
447
- })
448
-
449
- await Instance.provide({
450
- directory: tmp.path,
451
- fn: async () => {
452
- const config = await TuiConfig.get()
453
- expect(config.theme).toBe("resolved-theme")
454
- },
455
- })
456
- })
457
-
458
- test("loads managed tui config and gives it highest precedence", async () => {
459
- await using tmp = await tmpdir({
460
- init: async (dir) => {
461
- await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project-theme" }, null, 2))
462
- await fs.mkdir(managedConfigDir, { recursive: true })
463
- await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-theme" }, null, 2))
464
- },
465
- })
466
-
467
- await Instance.provide({
468
- directory: tmp.path,
469
- fn: async () => {
470
- const config = await TuiConfig.get()
471
- expect(config.theme).toBe("managed-theme")
472
- },
473
- })
474
- })
475
-
476
- test("loads .opencode/tui.json", async () => {
477
- await using tmp = await tmpdir({
478
- init: async (dir) => {
479
- await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
480
- await Bun.write(path.join(dir, ".opencode", "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2))
481
- },
482
- })
483
-
484
- await Instance.provide({
485
- directory: tmp.path,
486
- fn: async () => {
487
- const config = await TuiConfig.get()
488
- expect(config.diff_style).toBe("stacked")
489
- },
490
- })
491
- })
492
-
493
- test("gracefully falls back when tui.json has invalid JSON", async () => {
494
- await using tmp = await tmpdir({
495
- init: async (dir) => {
496
- await Bun.write(path.join(dir, "tui.json"), "{ invalid json }")
497
- await fs.mkdir(managedConfigDir, { recursive: true })
498
- await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-fallback" }, null, 2))
499
- },
500
- })
501
-
502
- await Instance.provide({
503
- directory: tmp.path,
504
- fn: async () => {
505
- const config = await TuiConfig.get()
506
- expect(config.theme).toBe("managed-fallback")
507
- expect(config.keybinds).toBeDefined()
508
- },
509
- })
510
- })
@@ -1,147 +0,0 @@
1
- import { afterEach, describe, expect, mock, test } from "bun:test"
2
- import { Identifier } from "../../src/id/id"
3
- import { Hono } from "hono"
4
- import { tmpdir } from "../fixture/fixture"
5
- import { Project } from "../../src/project/project"
6
- import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
7
- import { Instance } from "../../src/project/instance"
8
- import { Database } from "../../src/storage/db"
9
- import { resetDatabase } from "../fixture/db"
10
-
11
- afterEach(async () => {
12
- mock.restore()
13
- await resetDatabase()
14
- })
15
-
16
- type State = {
17
- workspace?: "first" | "second"
18
- calls: Array<{ method: string; url: string; body?: string }>
19
- }
20
-
21
- const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
22
-
23
- async function setup(state: State) {
24
- mock.module("../../src/control-plane/adaptors", () => ({
25
- getAdaptor: () => ({
26
- request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
27
- const body = data ? await new Response(data).text() : undefined
28
- state.calls.push({ method, url, body })
29
- return new Response("proxied", { status: 202 })
30
- },
31
- }),
32
- }))
33
-
34
- await using tmp = await tmpdir({ git: true })
35
- const { project } = await Project.fromDirectory(tmp.path)
36
-
37
- const id1 = Identifier.descending("workspace")
38
- const id2 = Identifier.descending("workspace")
39
-
40
- Database.use((db) =>
41
- db
42
- .insert(WorkspaceTable)
43
- .values([
44
- {
45
- id: id1,
46
- branch: "main",
47
- project_id: project.id,
48
- config: remote,
49
- },
50
- {
51
- id: id2,
52
- branch: "main",
53
- project_id: project.id,
54
- config: { type: "worktree", directory: tmp.path },
55
- },
56
- ])
57
- .run(),
58
- )
59
-
60
- const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
61
- const app = new Hono().use(SessionProxyMiddleware)
62
-
63
- return {
64
- id1,
65
- id2,
66
- app,
67
- async request(input: RequestInfo | URL, init?: RequestInit) {
68
- return Instance.provide({
69
- directory: state.workspace === "first" ? id1 : id2,
70
- fn: async () => app.request(input, init),
71
- })
72
- },
73
- }
74
- }
75
-
76
- describe("control-plane/session-proxy-middleware", () => {
77
- test("forwards non-GET session requests for remote workspaces", async () => {
78
- const state: State = {
79
- workspace: "first",
80
- calls: [],
81
- }
82
-
83
- const ctx = await setup(state)
84
-
85
- ctx.app.post("/session/foo", (c) => c.text("local", 200))
86
- const response = await ctx.request("http://workspace.test/session/foo?x=1", {
87
- method: "POST",
88
- body: JSON.stringify({ hello: "world" }),
89
- headers: {
90
- "content-type": "application/json",
91
- },
92
- })
93
-
94
- expect(response.status).toBe(202)
95
- expect(await response.text()).toBe("proxied")
96
- expect(state.calls).toEqual([
97
- {
98
- method: "POST",
99
- url: "/session/foo?x=1",
100
- body: '{"hello":"world"}',
101
- },
102
- ])
103
- })
104
-
105
- test("does not forward GET requests", async () => {
106
- const state: State = {
107
- workspace: "first",
108
- calls: [],
109
- }
110
-
111
- const ctx = await setup(state)
112
-
113
- ctx.app.get("/session/foo", (c) => c.text("local", 200))
114
- const response = await ctx.request("http://workspace.test/session/foo?x=1")
115
-
116
- expect(response.status).toBe(200)
117
- expect(await response.text()).toBe("local")
118
- expect(state.calls).toEqual([])
119
- })
120
-
121
- test("does not forward GET or POST requests for worktree workspaces", async () => {
122
- const state: State = {
123
- workspace: "second",
124
- calls: [],
125
- }
126
-
127
- const ctx = await setup(state)
128
-
129
- ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
130
- ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
131
-
132
- const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
133
- const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
134
- method: "POST",
135
- body: JSON.stringify({ hello: "world" }),
136
- headers: {
137
- "content-type": "application/json",
138
- },
139
- })
140
-
141
- expect(getResponse.status).toBe(200)
142
- expect(await getResponse.text()).toBe("local-get")
143
- expect(postResponse.status).toBe(200)
144
- expect(await postResponse.text()).toBe("local-post")
145
- expect(state.calls).toEqual([])
146
- })
147
- })
@@ -1,56 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test"
2
- import { parseSSE } from "../../src/control-plane/sse"
3
- import { resetDatabase } from "../fixture/db"
4
-
5
- afterEach(async () => {
6
- await resetDatabase()
7
- })
8
-
9
- function stream(chunks: string[]) {
10
- return new ReadableStream<Uint8Array>({
11
- start(controller) {
12
- const encoder = new TextEncoder()
13
- chunks.forEach((chunk) => controller.enqueue(encoder.encode(chunk)))
14
- controller.close()
15
- },
16
- })
17
- }
18
-
19
- describe("control-plane/sse", () => {
20
- test("parses JSON events with CRLF and multiline data blocks", async () => {
21
- const events: unknown[] = []
22
- const stop = new AbortController()
23
-
24
- await parseSSE(
25
- stream([
26
- 'data: {"type":"one","properties":{"ok":true}}\r\n\r\n',
27
- 'data: {"type":"two",\r\ndata: "properties":{"n":2}}\r\n\r\n',
28
- ]),
29
- stop.signal,
30
- (event) => events.push(event),
31
- )
32
-
33
- expect(events).toEqual([
34
- { type: "one", properties: { ok: true } },
35
- { type: "two", properties: { n: 2 } },
36
- ])
37
- })
38
-
39
- test("falls back to sse.message for non-json payload", async () => {
40
- const events: unknown[] = []
41
- const stop = new AbortController()
42
-
43
- await parseSSE(stream(["id: abc\nretry: 1500\ndata: hello world\n\n"]), stop.signal, (event) => events.push(event))
44
-
45
- expect(events).toEqual([
46
- {
47
- type: "sse.message",
48
- properties: {
49
- data: "hello world",
50
- id: "abc",
51
- retry: 1500,
52
- },
53
- },
54
- ])
55
- })
56
- })