@agentprojectcontext/apx 1.15.6 → 1.17.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 (222) hide show
  1. package/package.json +46 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent-langchain.js +296 -0
  28. package/src/daemon/super-agent.js +115 -19
  29. package/src/daemon/transcription.js +262 -59
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,456 @@
1
+ import { createMemo, createSignal, onMount, Show } from "solid-js"
2
+ import { useSync } from "@tui/context/sync"
3
+ import { map, pipe, sortBy } from "remeda"
4
+ import { DialogSelect } from "@tui/ui/dialog-select"
5
+ import { useDialog } from "@tui/ui/dialog"
6
+ import { useSDK } from "../context/sdk"
7
+ import { DialogPrompt } from "../ui/dialog-prompt"
8
+ import { Link } from "../ui/link"
9
+ import { useTheme } from "../context/theme"
10
+ import { TextAttributes } from "@opentui/core"
11
+ import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
12
+ import { DialogModel } from "./dialog-model"
13
+ import * as Clipboard from "@tui/util/clipboard"
14
+ import { useToast } from "../ui/toast"
15
+ import { isConsoleManagedProvider } from "@tui/util/provider-origin"
16
+ import { useConnected } from "./use-connected"
17
+ import { useBindings } from "../keymap"
18
+
19
+ const PROVIDER_PRIORITY: Record<string, number> = {
20
+ opencode: 0,
21
+ "opencode-go": 1,
22
+ openai: 2,
23
+ "github-copilot": 3,
24
+ anthropic: 4,
25
+ google: 5,
26
+ }
27
+
28
+ const CUSTOM_PROVIDER_OPTION_VALUE = "__opencode_custom_provider__"
29
+ const CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
30
+
31
+ type ProviderOptionBase = {
32
+ title: string
33
+ value: string
34
+ description?: string
35
+ category: string
36
+ }
37
+
38
+ type ProviderOption =
39
+ | (ProviderOptionBase & {
40
+ type: "provider"
41
+ providerID: string
42
+ })
43
+ | (ProviderOptionBase & {
44
+ type: "custom"
45
+ })
46
+
47
+ export function providerOptions(list: { id: string; name: string }[]): ProviderOption[] {
48
+ return [
49
+ ...pipe(
50
+ list,
51
+ sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
52
+ map((provider) => ({
53
+ type: "provider" as const,
54
+ title: provider.name,
55
+ value: provider.id,
56
+ providerID: provider.id,
57
+ description: {
58
+ opencode: "(Recommended)",
59
+ anthropic: "(API key)",
60
+ openai: "(ChatGPT Plus/Pro or API key)",
61
+ "opencode-go": "Low cost subscription for everyone",
62
+ }[provider.id],
63
+ category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Providers",
64
+ })),
65
+ ),
66
+ {
67
+ type: "custom",
68
+ title: "Other",
69
+ value: CUSTOM_PROVIDER_OPTION_VALUE,
70
+ description: "Custom provider",
71
+ category: "Providers",
72
+ },
73
+ ]
74
+ }
75
+
76
+ export function normalizeCustomProviderID(value: string) {
77
+ const providerID = value.trim().replace(/^@ai-sdk\//, "")
78
+ if (!CUSTOM_PROVIDER_ID.test(providerID)) return
79
+ return providerID
80
+ }
81
+
82
+ export function createDialogProviderOptions() {
83
+ const sync = useSync()
84
+ const dialog = useDialog()
85
+ const sdk = useSDK()
86
+ const toast = useToast()
87
+ const { theme } = useTheme()
88
+ const onboarded = useConnected()
89
+
90
+ async function promptCustomProviderID(): Promise<string | undefined> {
91
+ const value = await DialogPrompt.show(dialog, "Other", {
92
+ placeholder: "Provider id",
93
+ description: () => (
94
+ <text fg={theme.textMuted}>
95
+ This only stores a credential. Configure the provider in opencode.json to use it.
96
+ </text>
97
+ ),
98
+ })
99
+ if (value === null) return
100
+
101
+ const providerID = normalizeCustomProviderID(value)
102
+ if (providerID) return providerID
103
+
104
+ toast.show({
105
+ variant: "error",
106
+ message:
107
+ "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores",
108
+ })
109
+ return promptCustomProviderID()
110
+ }
111
+
112
+ const options = createMemo(() => {
113
+ return pipe(
114
+ providerOptions(sync.data.provider_next.all),
115
+ map((provider) => {
116
+ if (provider.type === "custom") {
117
+ return {
118
+ title: provider.title,
119
+ value: provider.value,
120
+ description: provider.description,
121
+ category: provider.category,
122
+ async onSelect() {
123
+ const providerID = await promptCustomProviderID()
124
+ if (!providerID) return
125
+ return dialog.replace(() => <ApiMethod providerID={providerID} title="API key" custom />)
126
+ },
127
+ }
128
+ }
129
+
130
+ const providerID = provider.providerID
131
+ const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID)
132
+ const connected = sync.data.provider_next.connected.includes(providerID)
133
+
134
+ return {
135
+ title: provider.title,
136
+ value: provider.value,
137
+ description: provider.description,
138
+ footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
139
+ category: provider.category,
140
+ gutter: connected && onboarded() ? () => <text fg={theme.success}>✓</text> : undefined,
141
+ async onSelect() {
142
+ if (consoleManaged) return
143
+
144
+ const methods = sync.data.provider_auth[providerID] ?? [
145
+ {
146
+ type: "api",
147
+ label: "API key",
148
+ },
149
+ ]
150
+ let index: number | null = 0
151
+ if (methods.length > 1) {
152
+ index = await new Promise<number | null>((resolve) => {
153
+ dialog.replace(
154
+ () => (
155
+ <DialogSelect
156
+ title="Select auth method"
157
+ options={methods.map((x, index) => ({
158
+ title: x.label,
159
+ value: index,
160
+ }))}
161
+ onSelect={(option) => resolve(option.value)}
162
+ />
163
+ ),
164
+ () => resolve(null),
165
+ )
166
+ })
167
+ }
168
+ if (index == null) return
169
+ const method = methods[index]
170
+ if (method.type === "oauth") {
171
+ let inputs: Record<string, string> | undefined
172
+ if (method.prompts?.length) {
173
+ const value = await PromptsMethod({
174
+ dialog,
175
+ prompts: method.prompts,
176
+ })
177
+ if (!value) return
178
+ inputs = value
179
+ }
180
+
181
+ const result = await sdk.client.provider.oauth.authorize({
182
+ providerID,
183
+ method: index,
184
+ inputs,
185
+ })
186
+ if (result.error) {
187
+ toast.show({
188
+ variant: "error",
189
+ message: JSON.stringify(result.error),
190
+ })
191
+ dialog.clear()
192
+ return
193
+ }
194
+ if (result.data?.method === "code") {
195
+ dialog.replace(() => (
196
+ <CodeMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />
197
+ ))
198
+ }
199
+ if (result.data?.method === "auto") {
200
+ dialog.replace(() => (
201
+ <AutoMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} />
202
+ ))
203
+ }
204
+ }
205
+ if (method.type === "api") {
206
+ let metadata: Record<string, string> | undefined
207
+ if (method.prompts?.length) {
208
+ const value = await PromptsMethod({ dialog, prompts: method.prompts })
209
+ if (!value) return
210
+ metadata = value
211
+ }
212
+ return dialog.replace(() => (
213
+ <ApiMethod providerID={providerID} title={method.label} metadata={metadata} />
214
+ ))
215
+ }
216
+ },
217
+ }
218
+ }),
219
+ )
220
+ })
221
+ return options
222
+ }
223
+
224
+ export function DialogProvider() {
225
+ const options = createDialogProviderOptions()
226
+ return <DialogSelect title="Connect a provider" options={options()} />
227
+ }
228
+
229
+ interface AutoMethodProps {
230
+ index: number
231
+ providerID: string
232
+ title: string
233
+ authorization: ProviderAuthAuthorization
234
+ }
235
+ function AutoMethod(props: AutoMethodProps) {
236
+ const { theme } = useTheme()
237
+ const sdk = useSDK()
238
+ const dialog = useDialog()
239
+ const sync = useSync()
240
+ const toast = useToast()
241
+
242
+ useBindings(() => ({
243
+ bindings: [
244
+ {
245
+ key: "c",
246
+ desc: "Copy provider code",
247
+ group: "Dialog",
248
+ cmd: () => {
249
+ const code =
250
+ props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url
251
+ Clipboard.copy(code)
252
+ .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
253
+ .catch(toast.error)
254
+ },
255
+ },
256
+ ],
257
+ }))
258
+
259
+ onMount(async () => {
260
+ const result = await sdk.client.provider.oauth.callback({
261
+ providerID: props.providerID,
262
+ method: props.index,
263
+ })
264
+ if (result.error) {
265
+ dialog.clear()
266
+ return
267
+ }
268
+ await sdk.client.instance.dispose()
269
+ await sync.bootstrap()
270
+ dialog.replace(() => <DialogModel providerID={props.providerID} />)
271
+ })
272
+
273
+ return (
274
+ <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
275
+ <box flexDirection="row" justifyContent="space-between">
276
+ <text attributes={TextAttributes.BOLD} fg={theme.text}>
277
+ {props.title}
278
+ </text>
279
+ <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
280
+ esc
281
+ </text>
282
+ </box>
283
+ <box gap={1}>
284
+ <Link href={props.authorization.url} fg={theme.primary} />
285
+ <text fg={theme.textMuted}>{props.authorization.instructions}</text>
286
+ </box>
287
+ <text fg={theme.textMuted}>Waiting for authorization...</text>
288
+ <text fg={theme.text}>
289
+ c <span style={{ fg: theme.textMuted }}>copy</span>
290
+ </text>
291
+ </box>
292
+ )
293
+ }
294
+
295
+ interface CodeMethodProps {
296
+ index: number
297
+ title: string
298
+ providerID: string
299
+ authorization: ProviderAuthAuthorization
300
+ }
301
+ function CodeMethod(props: CodeMethodProps) {
302
+ const { theme } = useTheme()
303
+ const sdk = useSDK()
304
+ const sync = useSync()
305
+ const dialog = useDialog()
306
+ const [error, setError] = createSignal(false)
307
+
308
+ return (
309
+ <DialogPrompt
310
+ title={props.title}
311
+ placeholder="Authorization code"
312
+ onConfirm={async (value) => {
313
+ const { error } = await sdk.client.provider.oauth.callback({
314
+ providerID: props.providerID,
315
+ method: props.index,
316
+ code: value,
317
+ })
318
+ if (!error) {
319
+ await sdk.client.instance.dispose()
320
+ await sync.bootstrap()
321
+ dialog.replace(() => <DialogModel providerID={props.providerID} />)
322
+ return
323
+ }
324
+ setError(true)
325
+ }}
326
+ description={() => (
327
+ <box gap={1}>
328
+ <text fg={theme.textMuted}>{props.authorization.instructions}</text>
329
+ <Link href={props.authorization.url} fg={theme.primary} />
330
+ <Show when={error()}>
331
+ <text fg={theme.error}>Invalid code</text>
332
+ </Show>
333
+ </box>
334
+ )}
335
+ />
336
+ )
337
+ }
338
+
339
+ interface ApiMethodProps {
340
+ providerID: string
341
+ title: string
342
+ metadata?: Record<string, string>
343
+ custom?: boolean
344
+ }
345
+ function ApiMethod(props: ApiMethodProps) {
346
+ const dialog = useDialog()
347
+ const sdk = useSDK()
348
+ const sync = useSync()
349
+ const toast = useToast()
350
+ const { theme } = useTheme()
351
+
352
+ return (
353
+ <DialogPrompt
354
+ title={props.title}
355
+ placeholder="API key"
356
+ description={
357
+ {
358
+ opencode: (
359
+ <box gap={1}>
360
+ <text fg={theme.textMuted}>
361
+ OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API
362
+ key.
363
+ </text>
364
+ <text fg={theme.text}>
365
+ Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
366
+ </text>
367
+ </box>
368
+ ),
369
+ "opencode-go": (
370
+ <box gap={1}>
371
+ <text fg={theme.textMuted}>
372
+ OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models
373
+ with generous usage limits.
374
+ </text>
375
+ <text fg={theme.text}>
376
+ Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> and enable OpenCode Go
377
+ </text>
378
+ </box>
379
+ ),
380
+ }[props.providerID] ?? undefined
381
+ }
382
+ onConfirm={async (value) => {
383
+ if (!value) return
384
+ await sdk.client.auth.set({
385
+ providerID: props.providerID,
386
+ auth: {
387
+ type: "api",
388
+ key: value,
389
+ ...(props.metadata ? { metadata: props.metadata } : {}),
390
+ },
391
+ })
392
+ await sdk.client.instance.dispose()
393
+ await sync.bootstrap()
394
+ if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) {
395
+ toast.show({
396
+ variant: "info",
397
+ message: `Saved credential for ${props.providerID}. Configure it in opencode.json to use it.`,
398
+ })
399
+ dialog.clear()
400
+ return
401
+ }
402
+ dialog.replace(() => <DialogModel providerID={props.providerID} />)
403
+ }}
404
+ />
405
+ )
406
+ }
407
+
408
+ interface PromptsMethodProps {
409
+ dialog: ReturnType<typeof useDialog>
410
+ prompts: NonNullable<ProviderAuthMethod["prompts"]>[number][]
411
+ }
412
+ async function PromptsMethod(props: PromptsMethodProps) {
413
+ const inputs: Record<string, string> = {}
414
+ for (const prompt of props.prompts) {
415
+ if (prompt.when) {
416
+ const value = inputs[prompt.when.key]
417
+ if (value === undefined) continue
418
+ const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value
419
+ if (!matches) continue
420
+ }
421
+
422
+ if (prompt.type === "select") {
423
+ const value = await new Promise<string | null>((resolve) => {
424
+ props.dialog.replace(
425
+ () => (
426
+ <DialogSelect
427
+ title={prompt.message}
428
+ options={prompt.options.map((x) => ({
429
+ title: x.label,
430
+ value: x.value,
431
+ description: x.hint,
432
+ }))}
433
+ onSelect={(option) => resolve(option.value)}
434
+ />
435
+ ),
436
+ () => resolve(null),
437
+ )
438
+ })
439
+ if (value === null) return null
440
+ inputs[prompt.key] = value
441
+ continue
442
+ }
443
+
444
+ const value = await new Promise<string | null>((resolve) => {
445
+ props.dialog.replace(
446
+ () => (
447
+ <DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={(value) => resolve(value)} />
448
+ ),
449
+ () => resolve(null),
450
+ )
451
+ })
452
+ if (value === null) return null
453
+ inputs[prompt.key] = value
454
+ }
455
+ return inputs
456
+ }
@@ -0,0 +1,160 @@
1
+ import { RGBA, TextAttributes } from "@opentui/core"
2
+ import open from "open"
3
+ import { createSignal } from "solid-js"
4
+ import { selectedForeground, useTheme } from "@tui/context/theme"
5
+ import { useDialog, type DialogContext } from "@tui/ui/dialog"
6
+ import { Link } from "@tui/ui/link"
7
+ import { BgPulse } from "./bg-pulse"
8
+ import { useBindings } from "../keymap"
9
+
10
+ const GO_URL = "https://opencode.ai/go"
11
+ const PAD_X = 3
12
+ const PAD_TOP_OUTER = 1
13
+ const FOREGROUND_ALPHA = 186
14
+
15
+ export type DialogRetryActionProps = {
16
+ title: string
17
+ message: string
18
+ label: string
19
+ link?: string
20
+ onClose?: (dontShowAgain?: boolean) => void
21
+ }
22
+
23
+ function runAction(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
24
+ if (props.link) open(props.link).catch(() => {})
25
+ props.onClose?.()
26
+ dialog.clear()
27
+ }
28
+
29
+ function dismiss(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
30
+ props.onClose?.(true)
31
+ dialog.clear()
32
+ }
33
+
34
+ function panelOverlay(color: RGBA) {
35
+ const [r, g, b] = color.toInts()
36
+ return RGBA.fromInts(r, g, b, FOREGROUND_ALPHA)
37
+ }
38
+
39
+ export function DialogRetryAction(props: DialogRetryActionProps) {
40
+ const dialog = useDialog()
41
+ const { theme } = useTheme()
42
+ const fg = selectedForeground(theme)
43
+ const showGoTreatment = () => props.link === GO_URL
44
+ const textBg = () => (showGoTreatment() ? panelOverlay(theme.backgroundPanel) : undefined)
45
+ const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
46
+
47
+ useBindings(() => ({
48
+ bindings: [
49
+ {
50
+ key: "left",
51
+ desc: "Previous retry option",
52
+ group: "Dialog",
53
+ cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
54
+ },
55
+ {
56
+ key: "right",
57
+ desc: "Next retry option",
58
+ group: "Dialog",
59
+ cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
60
+ },
61
+ {
62
+ key: "tab",
63
+ desc: "Next retry option",
64
+ group: "Dialog",
65
+ cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
66
+ },
67
+ {
68
+ key: "return",
69
+ desc: "Confirm retry option",
70
+ group: "Dialog",
71
+ cmd: () => {
72
+ if (selected() === "action") runAction(props, dialog)
73
+ else dismiss(props, dialog)
74
+ },
75
+ },
76
+ ],
77
+ }))
78
+
79
+ return (
80
+ <box>
81
+ {showGoTreatment() ? (
82
+ <box position="absolute" top={-PAD_TOP_OUTER} left={0} right={0} bottom={0} zIndex={0}>
83
+ <BgPulse />
84
+ </box>
85
+ ) : null}
86
+ <box zIndex={1} paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
87
+ <box flexDirection="row" justifyContent="space-between">
88
+ <text attributes={TextAttributes.BOLD} fg={theme.text} bg={textBg()}>
89
+ {props.title}
90
+ </text>
91
+ <text fg={theme.textMuted} bg={textBg()} onMouseUp={() => dialog.clear()}>
92
+ esc
93
+ </text>
94
+ </box>
95
+ <box gap={0}>
96
+ <text fg={theme.textMuted} bg={textBg()}>
97
+ {props.message}
98
+ </text>
99
+ </box>
100
+ {props.link ? (
101
+ showGoTreatment() ? (
102
+ <box alignItems="center" justifyContent="flex-end" height={7} paddingBottom={1}>
103
+ <Link href={props.link} fg={theme.primary} bg={textBg()} wrapMode="none" />
104
+ </box>
105
+ ) : (
106
+ <box width="100%" flexDirection="row" justifyContent="center" paddingBottom={1}>
107
+ <Link href={props.link} fg={theme.primary} wrapMode="none" />
108
+ </box>
109
+ )
110
+ ) : (
111
+ <box paddingBottom={1} />
112
+ )}
113
+ <box flexDirection="row" justifyContent="space-between">
114
+ <box
115
+ paddingLeft={2}
116
+ paddingRight={2}
117
+ backgroundColor={selected() === "dismiss" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
118
+ onMouseOver={() => setSelected("dismiss")}
119
+ onMouseUp={() => dismiss(props, dialog)}
120
+ >
121
+ <text
122
+ fg={selected() === "dismiss" ? fg : theme.textMuted}
123
+ bg={selected() === "dismiss" ? undefined : textBg()}
124
+ attributes={selected() === "dismiss" ? TextAttributes.BOLD : undefined}
125
+ >
126
+ don't show again
127
+ </text>
128
+ </box>
129
+ <box
130
+ paddingLeft={2}
131
+ paddingRight={2}
132
+ backgroundColor={selected() === "action" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
133
+ onMouseOver={() => setSelected("action")}
134
+ onMouseUp={() => runAction(props, dialog)}
135
+ >
136
+ <text
137
+ fg={selected() === "action" ? fg : theme.text}
138
+ bg={selected() === "action" ? undefined : textBg()}
139
+ attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
140
+ >
141
+ {props.label}
142
+ </text>
143
+ </box>
144
+ </box>
145
+ </box>
146
+ </box>
147
+ )
148
+ }
149
+
150
+ DialogRetryAction.show = (
151
+ dialog: DialogContext,
152
+ props: Pick<DialogRetryActionProps, "title" | "message" | "label" | "link">,
153
+ ) => {
154
+ return new Promise<boolean>((resolve) => {
155
+ dialog.replace(
156
+ () => <DialogRetryAction {...props} onClose={(dontShow) => resolve(dontShow ?? false)} />,
157
+ () => resolve(false),
158
+ )
159
+ })
160
+ }