@agentprojectcontext/apx 1.15.6 → 1.16.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 (221) hide show
  1. package/package.json +40 -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.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/whisper-server.py +57 -6
  30. package/src/overlay/index.html +44 -0
  31. package/src/overlay/main.js +480 -0
  32. package/src/overlay/package.json +3 -0
  33. package/src/overlay/preload.js +34 -0
  34. package/src/overlay/renderer.js +371 -0
  35. package/src/overlay/style.css +250 -0
  36. package/src/tui/_shims/cli-error.ts +6 -0
  37. package/src/tui/_shims/cli-logo.ts +18 -0
  38. package/src/tui/_shims/cli-ui.ts +1 -0
  39. package/src/tui/_shims/config-console-state.ts +7 -0
  40. package/src/tui/_shims/core-any.ts +30 -0
  41. package/src/tui/_shims/core-binary.ts +13 -0
  42. package/src/tui/_shims/core-flag.ts +3 -0
  43. package/src/tui/_shims/core-log.ts +14 -0
  44. package/src/tui/_shims/lsp-language.ts +1 -0
  45. package/src/tui/_shims/opencode-any.ts +135 -0
  46. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  47. package/src/tui/_shims/plugin-tui.ts +13 -0
  48. package/src/tui/_shims/provider-provider.ts +10 -0
  49. package/src/tui/_shims/session-retry.ts +1 -0
  50. package/src/tui/_shims/session-schema.ts +15 -0
  51. package/src/tui/_shims/session-session.ts +3 -0
  52. package/src/tui/_shims/snapshot.ts +4 -0
  53. package/src/tui/_shims/tool-any.ts +18 -0
  54. package/src/tui/_shims/util-error.ts +7 -0
  55. package/src/tui/_shims/util-filesystem.ts +79 -0
  56. package/src/tui/_shims/util-format.ts +7 -0
  57. package/src/tui/_shims/util-iife.ts +3 -0
  58. package/src/tui/_shims/util-locale.ts +10 -0
  59. package/src/tui/_shims/util-process.ts +38 -0
  60. package/src/tui/app.tsx +783 -0
  61. package/src/tui/asset/charge.wav +0 -0
  62. package/src/tui/asset/pulse-a.wav +0 -0
  63. package/src/tui/asset/pulse-b.wav +0 -0
  64. package/src/tui/asset/pulse-c.wav +0 -0
  65. package/src/tui/attach.ts +100 -0
  66. package/src/tui/component/bg-pulse-render.ts +436 -0
  67. package/src/tui/component/bg-pulse.tsx +99 -0
  68. package/src/tui/component/border.tsx +21 -0
  69. package/src/tui/component/dialog-agent.tsx +31 -0
  70. package/src/tui/component/dialog-console-org.tsx +103 -0
  71. package/src/tui/component/dialog-mcp.tsx +85 -0
  72. package/src/tui/component/dialog-model.tsx +175 -0
  73. package/src/tui/component/dialog-provider.tsx +456 -0
  74. package/src/tui/component/dialog-retry-action.tsx +160 -0
  75. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  76. package/src/tui/component/dialog-session-list.tsx +323 -0
  77. package/src/tui/component/dialog-session-rename.tsx +31 -0
  78. package/src/tui/component/dialog-skill.tsx +36 -0
  79. package/src/tui/component/dialog-stash.tsx +87 -0
  80. package/src/tui/component/dialog-status.tsx +168 -0
  81. package/src/tui/component/dialog-tag.tsx +44 -0
  82. package/src/tui/component/dialog-theme-list.tsx +50 -0
  83. package/src/tui/component/dialog-variant.tsx +39 -0
  84. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  85. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  86. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  87. package/src/tui/component/error-component.tsx +92 -0
  88. package/src/tui/component/logo.tsx +896 -0
  89. package/src/tui/component/plugin-route-missing.tsx +14 -0
  90. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  91. package/src/tui/component/prompt/cwd.ts +0 -0
  92. package/src/tui/component/prompt/frecency.tsx +90 -0
  93. package/src/tui/component/prompt/history.tsx +108 -0
  94. package/src/tui/component/prompt/index.tsx +1809 -0
  95. package/src/tui/component/prompt/part.ts +16 -0
  96. package/src/tui/component/prompt/stash.tsx +101 -0
  97. package/src/tui/component/prompt/traits.ts +35 -0
  98. package/src/tui/component/spinner.tsx +24 -0
  99. package/src/tui/component/startup-loading.tsx +63 -0
  100. package/src/tui/component/todo-item.tsx +32 -0
  101. package/src/tui/component/use-connected.tsx +9 -0
  102. package/src/tui/component/workspace-label.tsx +19 -0
  103. package/src/tui/config/cwd.ts +5 -0
  104. package/src/tui/config/keybind.ts +432 -0
  105. package/src/tui/config/tui-migrate.ts +154 -0
  106. package/src/tui/config/tui-schema.ts +34 -0
  107. package/src/tui/config/tui.ts +46 -0
  108. package/src/tui/context/aggregate-failures.ts +34 -0
  109. package/src/tui/context/args.tsx +15 -0
  110. package/src/tui/context/command-palette.tsx +163 -0
  111. package/src/tui/context/directory.ts +15 -0
  112. package/src/tui/context/editor-zed.ts +283 -0
  113. package/src/tui/context/editor.ts +468 -0
  114. package/src/tui/context/event-apx.ts +22 -0
  115. package/src/tui/context/event.ts +6 -0
  116. package/src/tui/context/exit.tsx +60 -0
  117. package/src/tui/context/helper.tsx +25 -0
  118. package/src/tui/context/kv.tsx +81 -0
  119. package/src/tui/context/local.tsx +608 -0
  120. package/src/tui/context/path-format.tsx +39 -0
  121. package/src/tui/context/project-apx.tsx +48 -0
  122. package/src/tui/context/project.tsx +7 -0
  123. package/src/tui/context/prompt.tsx +18 -0
  124. package/src/tui/context/route.tsx +52 -0
  125. package/src/tui/context/sdk-apx.tsx +185 -0
  126. package/src/tui/context/sdk.tsx +6 -0
  127. package/src/tui/context/sync-apx.tsx +178 -0
  128. package/src/tui/context/sync-v2.tsx +16 -0
  129. package/src/tui/context/sync.tsx +118 -0
  130. package/src/tui/context/theme/aura.json +69 -0
  131. package/src/tui/context/theme/ayu.json +80 -0
  132. package/src/tui/context/theme/carbonfox.json +248 -0
  133. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  134. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  135. package/src/tui/context/theme/catppuccin.json +112 -0
  136. package/src/tui/context/theme/cobalt2.json +225 -0
  137. package/src/tui/context/theme/cursor.json +249 -0
  138. package/src/tui/context/theme/dracula.json +219 -0
  139. package/src/tui/context/theme/everforest.json +241 -0
  140. package/src/tui/context/theme/flexoki.json +237 -0
  141. package/src/tui/context/theme/github.json +233 -0
  142. package/src/tui/context/theme/gruvbox.json +242 -0
  143. package/src/tui/context/theme/kanagawa.json +77 -0
  144. package/src/tui/context/theme/lucent-orng.json +234 -0
  145. package/src/tui/context/theme/material.json +235 -0
  146. package/src/tui/context/theme/matrix.json +77 -0
  147. package/src/tui/context/theme/mercury.json +252 -0
  148. package/src/tui/context/theme/monokai.json +221 -0
  149. package/src/tui/context/theme/nightowl.json +221 -0
  150. package/src/tui/context/theme/nord.json +223 -0
  151. package/src/tui/context/theme/one-dark.json +84 -0
  152. package/src/tui/context/theme/opencode.json +245 -0
  153. package/src/tui/context/theme/orng.json +249 -0
  154. package/src/tui/context/theme/osaka-jade.json +93 -0
  155. package/src/tui/context/theme/palenight.json +222 -0
  156. package/src/tui/context/theme/rosepine.json +234 -0
  157. package/src/tui/context/theme/solarized.json +223 -0
  158. package/src/tui/context/theme/synthwave84.json +226 -0
  159. package/src/tui/context/theme/tokyonight.json +243 -0
  160. package/src/tui/context/theme/vercel.json +245 -0
  161. package/src/tui/context/theme/vesper.json +218 -0
  162. package/src/tui/context/theme/zenburn.json +223 -0
  163. package/src/tui/context/theme.tsx +1247 -0
  164. package/src/tui/context/tui-config.tsx +9 -0
  165. package/src/tui/event.ts +16 -0
  166. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  167. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  168. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  169. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  170. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  171. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  172. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  173. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  174. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  175. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  176. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  177. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  178. package/src/tui/keymap.tsx +166 -0
  179. package/src/tui/layer.ts +6 -0
  180. package/src/tui/plugin/api.tsx +381 -0
  181. package/src/tui/plugin/command-shim.ts +109 -0
  182. package/src/tui/plugin/internal.ts +33 -0
  183. package/src/tui/plugin/runtime.ts +1069 -0
  184. package/src/tui/plugin/slots.tsx +60 -0
  185. package/src/tui/routes/home.tsx +96 -0
  186. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  187. package/src/tui/routes/session/dialog-message.tsx +108 -0
  188. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  189. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  190. package/src/tui/routes/session/footer.tsx +91 -0
  191. package/src/tui/routes/session/index.tsx +188 -0
  192. package/src/tui/routes/session/permission.tsx +722 -0
  193. package/src/tui/routes/session/question.tsx +490 -0
  194. package/src/tui/routes/session/sidebar.tsx +102 -0
  195. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  196. package/src/tui/run.ts +84 -0
  197. package/src/tui/thread.ts +261 -0
  198. package/src/tui/tsconfig.json +40 -0
  199. package/src/tui/ui/dialog-alert.tsx +66 -0
  200. package/src/tui/ui/dialog-confirm.tsx +108 -0
  201. package/src/tui/ui/dialog-export-options.tsx +217 -0
  202. package/src/tui/ui/dialog-help.tsx +40 -0
  203. package/src/tui/ui/dialog-prompt.tsx +101 -0
  204. package/src/tui/ui/dialog-select.tsx +553 -0
  205. package/src/tui/ui/dialog.tsx +211 -0
  206. package/src/tui/ui/link.tsx +34 -0
  207. package/src/tui/ui/spinner.ts +368 -0
  208. package/src/tui/ui/toast.tsx +111 -0
  209. package/src/tui/util/clipboard.ts +217 -0
  210. package/src/tui/util/editor.ts +37 -0
  211. package/src/tui/util/model.ts +23 -0
  212. package/src/tui/util/provider-origin.ts +7 -0
  213. package/src/tui/util/revert-diff.ts +18 -0
  214. package/src/tui/util/scroll.ts +25 -0
  215. package/src/tui/util/selection.ts +65 -0
  216. package/src/tui/util/signal.ts +41 -0
  217. package/src/tui/util/sound.ts +156 -0
  218. package/src/tui/util/transcript.ts +112 -0
  219. package/src/tui/validate-session.ts +29 -0
  220. package/src/tui/win32.ts +130 -0
  221. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,722 @@
1
+ import { createStore } from "solid-js/store"
2
+ import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
3
+ import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
4
+ import type { TextareaRenderable } from "@opentui/core"
5
+ import { useTheme, selectedForeground } from "../../context/theme"
6
+ import type { PermissionRequest } from "@opencode-ai/sdk/v2"
7
+ import { useSDK } from "../../context/sdk"
8
+ import { SplitBorder } from "../../component/border"
9
+ import { useSync } from "../../context/sync"
10
+ import { useProject } from "../../context/project"
11
+ import path from "path"
12
+ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
13
+ import { Locale } from "@/util/locale"
14
+ import { ShellID } from "@/tool/shell/id"
15
+ import { webSearchProviderLabel } from "@/tool/websearch"
16
+ import { useDialog } from "../../ui/dialog"
17
+ import { getScrollAcceleration } from "../../util/scroll"
18
+ import { useTuiConfig } from "../../context/tui-config"
19
+ import { useBindings, useCommandShortcut } from "../../keymap"
20
+ import { usePathFormatter } from "../../context/path-format"
21
+
22
+ type PermissionStage = "permission" | "always" | "reject"
23
+
24
+ function filetype(input?: string) {
25
+ if (!input) return "none"
26
+ const ext = path.extname(input)
27
+ const language = LANGUAGE_EXTENSIONS[ext]
28
+ if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
29
+ return language
30
+ }
31
+
32
+ function EditBody(props: { request: PermissionRequest }) {
33
+ const themeState = useTheme()
34
+ const theme = themeState.theme
35
+ const syntax = themeState.syntax
36
+ const config = useTuiConfig()
37
+ const dimensions = useTerminalDimensions()
38
+
39
+ const filepath = createMemo(() => (props.request.metadata?.filepath as string) ?? "")
40
+ const diff = createMemo(() => (props.request.metadata?.diff as string) ?? "")
41
+
42
+ const view = createMemo(() => {
43
+ const diffStyle = config.diff_style
44
+ if (diffStyle === "stacked") return "unified"
45
+ return dimensions().width > 120 ? "split" : "unified"
46
+ })
47
+
48
+ const ft = createMemo(() => filetype(filepath()))
49
+ const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
50
+
51
+ return (
52
+ <box flexDirection="column" gap={1}>
53
+ <Show when={diff()}>
54
+ <scrollbox
55
+ height="100%"
56
+ scrollAcceleration={scrollAcceleration()}
57
+ verticalScrollbarOptions={{
58
+ trackOptions: {
59
+ backgroundColor: theme.background,
60
+ foregroundColor: theme.borderActive,
61
+ },
62
+ }}
63
+ >
64
+ <diff
65
+ diff={diff()}
66
+ view={view()}
67
+ filetype={ft()}
68
+ syntaxStyle={syntax()}
69
+ showLineNumbers={true}
70
+ width="100%"
71
+ wrapMode="word"
72
+ fg={theme.text}
73
+ addedBg={theme.diffAddedBg}
74
+ removedBg={theme.diffRemovedBg}
75
+ contextBg={theme.diffContextBg}
76
+ addedSignColor={theme.diffHighlightAdded}
77
+ removedSignColor={theme.diffHighlightRemoved}
78
+ lineNumberFg={theme.diffLineNumber}
79
+ lineNumberBg={theme.diffContextBg}
80
+ addedLineNumberBg={theme.diffAddedLineNumberBg}
81
+ removedLineNumberBg={theme.diffRemovedLineNumberBg}
82
+ />
83
+ </scrollbox>
84
+ </Show>
85
+ <Show when={!diff()}>
86
+ <box paddingLeft={1}>
87
+ <text fg={theme.textMuted}>No diff provided</text>
88
+ </box>
89
+ </Show>
90
+ </box>
91
+ )
92
+ }
93
+
94
+ function TextBody(props: { title: string; description?: string; icon?: string }) {
95
+ const { theme } = useTheme()
96
+ return (
97
+ <>
98
+ <box flexDirection="row" gap={1} paddingLeft={1}>
99
+ <Show when={props.icon}>
100
+ <text fg={theme.textMuted} flexShrink={0}>
101
+ {props.icon}
102
+ </text>
103
+ </Show>
104
+ <text fg={theme.textMuted}>{props.title}</text>
105
+ </box>
106
+ <Show when={props.description}>
107
+ <box paddingLeft={1}>
108
+ <text fg={theme.text}>{props.description}</text>
109
+ </box>
110
+ </Show>
111
+ </>
112
+ )
113
+ }
114
+
115
+ export function PermissionPrompt(props: { request: PermissionRequest }) {
116
+ const sdk = useSDK()
117
+ const project = useProject()
118
+ const sync = useSync()
119
+ const [store, setStore] = createStore({
120
+ stage: "permission" as PermissionStage,
121
+ })
122
+ const pathFormatter = usePathFormatter()
123
+
124
+ const session = createMemo(() => sync.data.session.find((s) => s.id === props.request.sessionID))
125
+
126
+ const input = createMemo(() => {
127
+ const tool = props.request.tool
128
+ if (!tool) return {}
129
+ const parts = sync.data.part[tool.messageID] ?? []
130
+ for (const part of parts) {
131
+ if (part.type === "tool" && part.callID === tool.callID && part.state.status !== "pending") {
132
+ return part.state.input ?? {}
133
+ }
134
+ }
135
+ return {}
136
+ })
137
+
138
+ const { theme } = useTheme()
139
+
140
+ return (
141
+ <Switch>
142
+ <Match when={store.stage === "always"}>
143
+ <Prompt
144
+ title="Always allow"
145
+ body={
146
+ <Switch>
147
+ <Match when={props.request.always.length === 1 && props.request.always[0] === "*"}>
148
+ <TextBody title={"This will allow " + props.request.permission + " until OpenCode is restarted."} />
149
+ </Match>
150
+ <Match when={true}>
151
+ <box paddingLeft={1} gap={1}>
152
+ <text fg={theme.textMuted}>This will allow the following patterns until OpenCode is restarted</text>
153
+ <box>
154
+ <For each={props.request.always}>
155
+ {(pattern) => (
156
+ <text fg={theme.text}>
157
+ {"- "}
158
+ {pattern}
159
+ </text>
160
+ )}
161
+ </For>
162
+ </box>
163
+ </box>
164
+ </Match>
165
+ </Switch>
166
+ }
167
+ options={{ confirm: "Confirm", cancel: "Cancel" }}
168
+ escapeKey="cancel"
169
+ onSelect={(option) => {
170
+ setStore("stage", "permission")
171
+ if (option === "cancel") return
172
+ void sdk.client.permission.reply({
173
+ reply: "always",
174
+ requestID: props.request.id,
175
+ workspace: project.workspace.current(),
176
+ })
177
+ }}
178
+ />
179
+ </Match>
180
+ <Match when={store.stage === "reject"}>
181
+ <RejectPrompt
182
+ onConfirm={(message) => {
183
+ void sdk.client.permission.reply({
184
+ reply: "reject",
185
+ requestID: props.request.id,
186
+ message: message || undefined,
187
+ workspace: project.workspace.current(),
188
+ })
189
+ }}
190
+ onCancel={() => {
191
+ setStore("stage", "permission")
192
+ }}
193
+ />
194
+ </Match>
195
+ <Match when={store.stage === "permission"}>
196
+ {(() => {
197
+ const info = () => {
198
+ const permission = props.request.permission
199
+ const data = input()
200
+
201
+ if (permission === "edit") {
202
+ const raw = props.request.metadata?.filepath
203
+ const filepath = typeof raw === "string" ? raw : ""
204
+ return {
205
+ icon: "→",
206
+ title: `Edit ${pathFormatter.format(filepath)}`,
207
+ body: <EditBody request={props.request} />,
208
+ }
209
+ }
210
+
211
+ if (permission === "read") {
212
+ const raw = data.filePath
213
+ const filePath = typeof raw === "string" ? raw : ""
214
+ return {
215
+ icon: "→",
216
+ title: `Read ${pathFormatter.format(filePath)}`,
217
+ body: (
218
+ <Show when={filePath}>
219
+ <box paddingLeft={1}>
220
+ <text fg={theme.textMuted}>{"Path: " + pathFormatter.format(filePath)}</text>
221
+ </box>
222
+ </Show>
223
+ ),
224
+ }
225
+ }
226
+
227
+ if (permission === "glob") {
228
+ const pattern = typeof data.pattern === "string" ? data.pattern : ""
229
+ return {
230
+ icon: "✱",
231
+ title: `Glob "${pattern}"`,
232
+ body: (
233
+ <Show when={pattern}>
234
+ <box paddingLeft={1}>
235
+ <text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
236
+ </box>
237
+ </Show>
238
+ ),
239
+ }
240
+ }
241
+
242
+ if (permission === "grep") {
243
+ const pattern = typeof data.pattern === "string" ? data.pattern : ""
244
+ return {
245
+ icon: "✱",
246
+ title: `Grep "${pattern}"`,
247
+ body: (
248
+ <Show when={pattern}>
249
+ <box paddingLeft={1}>
250
+ <text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
251
+ </box>
252
+ </Show>
253
+ ),
254
+ }
255
+ }
256
+
257
+ if (permission === "list") {
258
+ const raw = data.path
259
+ const dir = typeof raw === "string" ? raw : ""
260
+ return {
261
+ icon: "→",
262
+ title: `List ${pathFormatter.format(dir)}`,
263
+ body: (
264
+ <Show when={dir}>
265
+ <box paddingLeft={1}>
266
+ <text fg={theme.textMuted}>{"Path: " + pathFormatter.format(dir)}</text>
267
+ </box>
268
+ </Show>
269
+ ),
270
+ }
271
+ }
272
+
273
+ if (permission === ShellID.ToolID) {
274
+ const title =
275
+ typeof data.description === "string" && data.description ? data.description : "Shell command"
276
+ const command = typeof data.command === "string" ? data.command : ""
277
+ return {
278
+ icon: "#",
279
+ title,
280
+ body: (
281
+ <Show when={command}>
282
+ <box paddingLeft={1}>
283
+ <text fg={theme.text}>{"$ " + command}</text>
284
+ </box>
285
+ </Show>
286
+ ),
287
+ }
288
+ }
289
+
290
+ if (permission === "task") {
291
+ const type = typeof data.subagent_type === "string" ? data.subagent_type : "Unknown"
292
+ const desc = typeof data.description === "string" ? data.description : ""
293
+ return {
294
+ icon: "#",
295
+ title: `${Locale.titlecase(type)} Task`,
296
+ body: (
297
+ <Show when={desc}>
298
+ <box paddingLeft={1}>
299
+ <text fg={theme.text}>{"◉ " + desc}</text>
300
+ </box>
301
+ </Show>
302
+ ),
303
+ }
304
+ }
305
+
306
+ if (permission === "webfetch") {
307
+ const url = typeof data.url === "string" ? data.url : ""
308
+ return {
309
+ icon: "%",
310
+ title: `WebFetch ${url}`,
311
+ body: (
312
+ <Show when={url}>
313
+ <box paddingLeft={1}>
314
+ <text fg={theme.textMuted}>{"URL: " + url}</text>
315
+ </box>
316
+ </Show>
317
+ ),
318
+ }
319
+ }
320
+
321
+ if (permission === "websearch") {
322
+ const query = typeof data.query === "string" ? data.query : ""
323
+ return {
324
+ icon: "◈",
325
+ title: `${webSearchProviderLabel(data.provider)} "${query}"`,
326
+ body: (
327
+ <Show when={query}>
328
+ <box paddingLeft={1}>
329
+ <text fg={theme.textMuted}>{"Query: " + query}</text>
330
+ </box>
331
+ </Show>
332
+ ),
333
+ }
334
+ }
335
+
336
+ if (permission === "external_directory") {
337
+ const meta = props.request.metadata ?? {}
338
+ const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
339
+ const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
340
+ const pattern = props.request.patterns?.[0]
341
+ const derived =
342
+ typeof pattern === "string" ? (pattern.includes("*") ? path.dirname(pattern) : pattern) : undefined
343
+
344
+ const raw = parent ?? filepath ?? derived
345
+ const dir = pathFormatter.format(raw)
346
+ const patterns = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string")
347
+
348
+ return {
349
+ icon: "←",
350
+ title: `Access external directory ${dir}`,
351
+ body: (
352
+ <Show when={patterns.length > 0}>
353
+ <box paddingLeft={1} gap={1}>
354
+ <text fg={theme.textMuted}>Patterns</text>
355
+ <box>
356
+ <For each={patterns}>{(p) => <text fg={theme.text}>{"- " + p}</text>}</For>
357
+ </box>
358
+ </box>
359
+ </Show>
360
+ ),
361
+ }
362
+ }
363
+
364
+ if (permission === "doom_loop") {
365
+ return {
366
+ icon: "⟳",
367
+ title: "Continue after repeated failures",
368
+ body: (
369
+ <box paddingLeft={1}>
370
+ <text fg={theme.textMuted}>This keeps the session running despite repeated failures.</text>
371
+ </box>
372
+ ),
373
+ }
374
+ }
375
+
376
+ return {
377
+ icon: "⚙",
378
+ title: `Call tool ${permission}`,
379
+ body: (
380
+ <box paddingLeft={1}>
381
+ <text fg={theme.textMuted}>{"Tool: " + permission}</text>
382
+ </box>
383
+ ),
384
+ }
385
+ }
386
+
387
+ const current = info()
388
+
389
+ const header = () => (
390
+ <box flexDirection="column" gap={0}>
391
+ <box flexDirection="row" gap={1} flexShrink={0}>
392
+ <text fg={theme.warning}>{"△"}</text>
393
+ <text fg={theme.text}>Permission required</text>
394
+ </box>
395
+ <box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
396
+ <text fg={theme.textMuted} flexShrink={0}>
397
+ {current.icon}
398
+ </text>
399
+ <text fg={theme.text}>{current.title}</text>
400
+ </box>
401
+ </box>
402
+ )
403
+
404
+ const body = (
405
+ <Prompt
406
+ title="Permission required"
407
+ header={header()}
408
+ body={current.body}
409
+ options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
410
+ escapeKey="reject"
411
+ fullscreen
412
+ onSelect={(option) => {
413
+ if (option === "always") {
414
+ setStore("stage", "always")
415
+ return
416
+ }
417
+ if (option === "reject") {
418
+ if (session()?.parentID) {
419
+ setStore("stage", "reject")
420
+ return
421
+ }
422
+ void sdk.client.permission.reply({
423
+ reply: "reject",
424
+ requestID: props.request.id,
425
+ workspace: project.workspace.current(),
426
+ })
427
+ return
428
+ }
429
+ void sdk.client.permission.reply({
430
+ reply: "once",
431
+ requestID: props.request.id,
432
+ workspace: project.workspace.current(),
433
+ })
434
+ }}
435
+ />
436
+ )
437
+
438
+ return body
439
+ })()}
440
+ </Match>
441
+ </Switch>
442
+ )
443
+ }
444
+
445
+ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: () => void }) {
446
+ let input: TextareaRenderable
447
+ const { theme } = useTheme()
448
+ const tuiConfig = useTuiConfig()
449
+ const dimensions = useTerminalDimensions()
450
+ const narrow = createMemo(() => dimensions().width < 80)
451
+ const dialog = useDialog()
452
+ useBindings(() => ({
453
+ enabled: dialog.stack.length === 0,
454
+ commands: [
455
+ {
456
+ name: "app.exit",
457
+ title: "Cancel permission rejection",
458
+ category: "Permission",
459
+ run() {
460
+ props.onCancel()
461
+ },
462
+ },
463
+ ],
464
+ bindings: [
465
+ { key: "escape", desc: "Cancel permission rejection", group: "Permission", cmd: () => props.onCancel() },
466
+ ...tuiConfig.keybinds.get("app.exit"),
467
+ {
468
+ key: "return",
469
+ desc: "Confirm permission rejection",
470
+ group: "Permission",
471
+ cmd: () => props.onConfirm(input.plainText),
472
+ },
473
+ ],
474
+ }))
475
+
476
+ return (
477
+ <box
478
+ backgroundColor={theme.backgroundPanel}
479
+ border={["left"]}
480
+ borderColor={theme.error}
481
+ customBorderChars={SplitBorder.customBorderChars}
482
+ >
483
+ <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
484
+ <box flexDirection="row" gap={1} paddingLeft={1}>
485
+ <text fg={theme.error}>{"△"}</text>
486
+ <text fg={theme.text}>Reject permission</text>
487
+ </box>
488
+ <box paddingLeft={1}>
489
+ <text fg={theme.textMuted}>Tell OpenCode what to do differently</text>
490
+ </box>
491
+ </box>
492
+ <box
493
+ flexDirection={narrow() ? "column" : "row"}
494
+ flexShrink={0}
495
+ paddingTop={1}
496
+ paddingLeft={2}
497
+ paddingRight={3}
498
+ paddingBottom={1}
499
+ backgroundColor={theme.backgroundElement}
500
+ justifyContent={narrow() ? "flex-start" : "space-between"}
501
+ alignItems={narrow() ? "flex-start" : "center"}
502
+ gap={1}
503
+ >
504
+ <textarea
505
+ ref={(val: TextareaRenderable) => {
506
+ input = val
507
+ val.traits = { status: "REJECT" }
508
+ }}
509
+ focused
510
+ textColor={theme.text}
511
+ focusedTextColor={theme.text}
512
+ cursorColor={theme.primary}
513
+ />
514
+ <box flexDirection="row" gap={2} flexShrink={0}>
515
+ <text fg={theme.text}>
516
+ enter <span style={{ fg: theme.textMuted }}>confirm</span>
517
+ </text>
518
+ <text fg={theme.text}>
519
+ esc <span style={{ fg: theme.textMuted }}>cancel</span>
520
+ </text>
521
+ </box>
522
+ </box>
523
+ </box>
524
+ )
525
+ }
526
+
527
+ function Prompt<const T extends Record<string, string>>(props: {
528
+ title: string
529
+ header?: JSX.Element
530
+ body: JSX.Element
531
+ options: T
532
+ escapeKey?: keyof T
533
+ fullscreen?: boolean
534
+ onSelect: (option: keyof T) => void
535
+ }) {
536
+ const { theme } = useTheme()
537
+ const tuiConfig = useTuiConfig()
538
+ const dimensions = useTerminalDimensions()
539
+ const keys = Object.keys(props.options) as (keyof T)[]
540
+ const [store, setStore] = createStore({
541
+ selected: keys[0],
542
+ expanded: false,
543
+ })
544
+ const narrow = createMemo(() => dimensions().width < 80)
545
+ const dialog = useDialog()
546
+ const fullscreenHint = useCommandShortcut("permission.prompt.fullscreen")
547
+
548
+ useBindings(() => ({
549
+ enabled: dialog.stack.length === 0,
550
+ commands: [
551
+ {
552
+ name: "app.exit",
553
+ title: "Reject permission",
554
+ category: "Permission",
555
+ run() {
556
+ if (!props.escapeKey) return
557
+ props.onSelect(props.escapeKey)
558
+ },
559
+ },
560
+ {
561
+ name: "permission.prompt.fullscreen",
562
+ title: "Toggle permission fullscreen",
563
+ category: "Permission",
564
+ run() {
565
+ if (!props.fullscreen) return
566
+ setStore("expanded", (v) => !v)
567
+ },
568
+ },
569
+ ],
570
+ bindings: [
571
+ {
572
+ key: "left",
573
+ desc: "Previous permission option",
574
+ group: "Permission",
575
+ cmd: () => {
576
+ const idx = keys.indexOf(store.selected)
577
+ const next = keys[(idx - 1 + keys.length) % keys.length]
578
+ setStore("selected", next)
579
+ },
580
+ },
581
+ {
582
+ key: "h",
583
+ desc: "Previous permission option",
584
+ group: "Permission",
585
+ cmd: () => {
586
+ const idx = keys.indexOf(store.selected)
587
+ const next = keys[(idx - 1 + keys.length) % keys.length]
588
+ setStore("selected", next)
589
+ },
590
+ },
591
+ {
592
+ key: "right",
593
+ desc: "Next permission option",
594
+ group: "Permission",
595
+ cmd: () => {
596
+ const idx = keys.indexOf(store.selected)
597
+ const next = keys[(idx + 1) % keys.length]
598
+ setStore("selected", next)
599
+ },
600
+ },
601
+ {
602
+ key: "l",
603
+ desc: "Next permission option",
604
+ group: "Permission",
605
+ cmd: () => {
606
+ const idx = keys.indexOf(store.selected)
607
+ const next = keys[(idx + 1) % keys.length]
608
+ setStore("selected", next)
609
+ },
610
+ },
611
+ {
612
+ key: "return",
613
+ desc: "Select permission option",
614
+ group: "Permission",
615
+ cmd: () => props.onSelect(store.selected),
616
+ },
617
+ ...(props.escapeKey
618
+ ? [
619
+ {
620
+ key: "escape",
621
+ desc: "Reject permission",
622
+ group: "Permission",
623
+ cmd: () => props.onSelect(props.escapeKey!),
624
+ },
625
+ ]
626
+ : []),
627
+ ...(props.escapeKey ? tuiConfig.keybinds.get("app.exit") : []),
628
+ ...(props.fullscreen ? tuiConfig.keybinds.get("permission.prompt.fullscreen") : []),
629
+ ],
630
+ }))
631
+
632
+ const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen"))
633
+ useRenderer()
634
+
635
+ const content = () => (
636
+ <box
637
+ backgroundColor={theme.backgroundPanel}
638
+ border={["left"]}
639
+ borderColor={theme.warning}
640
+ customBorderChars={SplitBorder.customBorderChars}
641
+ {...(store.expanded
642
+ ? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" }
643
+ : {
644
+ top: 0,
645
+ maxHeight: 15,
646
+ bottom: 0,
647
+ left: 0,
648
+ right: 0,
649
+ position: "relative",
650
+ })}
651
+ >
652
+ <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
653
+ <Show
654
+ when={props.header}
655
+ fallback={
656
+ <box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
657
+ <text fg={theme.warning}>{"△"}</text>
658
+ <text fg={theme.text}>{props.title}</text>
659
+ </box>
660
+ }
661
+ >
662
+ <box paddingLeft={1} flexShrink={0}>
663
+ {props.header}
664
+ </box>
665
+ </Show>
666
+ {props.body}
667
+ </box>
668
+ <box
669
+ flexDirection={narrow() ? "column" : "row"}
670
+ flexShrink={0}
671
+ gap={1}
672
+ paddingTop={1}
673
+ paddingLeft={2}
674
+ paddingRight={3}
675
+ paddingBottom={1}
676
+ backgroundColor={theme.backgroundElement}
677
+ justifyContent={narrow() ? "flex-start" : "space-between"}
678
+ alignItems={narrow() ? "flex-start" : "center"}
679
+ >
680
+ <box flexDirection="row" gap={1} flexShrink={0}>
681
+ <For each={keys}>
682
+ {(option) => (
683
+ <box
684
+ paddingLeft={1}
685
+ paddingRight={1}
686
+ backgroundColor={option === store.selected ? theme.warning : theme.backgroundMenu}
687
+ onMouseOver={() => setStore("selected", option)}
688
+ onMouseUp={() => {
689
+ setStore("selected", option)
690
+ props.onSelect(option)
691
+ }}
692
+ >
693
+ <text fg={option === store.selected ? selectedForeground(theme, theme.warning) : theme.textMuted}>
694
+ {props.options[option]}
695
+ </text>
696
+ </box>
697
+ )}
698
+ </For>
699
+ </box>
700
+ <box flexDirection="row" gap={2} flexShrink={0}>
701
+ <Show when={props.fullscreen}>
702
+ <text fg={theme.text}>
703
+ {fullscreenHint()} <span style={{ fg: theme.textMuted }}>{hint()}</span>
704
+ </text>
705
+ </Show>
706
+ <text fg={theme.text}>
707
+ {"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
708
+ </text>
709
+ <text fg={theme.text}>
710
+ enter <span style={{ fg: theme.textMuted }}>confirm</span>
711
+ </text>
712
+ </box>
713
+ </box>
714
+ </box>
715
+ )
716
+
717
+ return (
718
+ <Show when={!store.expanded} fallback={<Portal>{content()}</Portal>}>
719
+ {content()}
720
+ </Show>
721
+ )
722
+ }