@neurocode-ai/tui 1.18.8

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