@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
package/src/app.tsx ADDED
@@ -0,0 +1,1134 @@
1
+ import { render, TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid"
2
+ import { registerNeurocodeSpinner } from "./component/register-spinner"
3
+ import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
4
+ import { Deferred, Effect } from "effect"
5
+ import { Global } from "@neurocode-ai/core/global"
6
+ import { Flag } from "@neurocode-ai/core/flag/flag"
7
+ import { InstallationVersion } from "@neurocode-ai/core/installation/version"
8
+ import { ClipboardProvider, useClipboard } from "./context/clipboard"
9
+ import { ExitProvider, useExit } from "./context/exit"
10
+ import { EpilogueProvider } from "./context/epilogue"
11
+ import * as Selection from "./util/selection"
12
+ import { createCliRenderer, MouseButton } from "@opentui/core"
13
+ import { RouteProvider, useRoute } from "./context/route"
14
+ import {
15
+ Switch,
16
+ Match,
17
+ createEffect,
18
+ createMemo,
19
+ ErrorBoundary,
20
+ createSignal,
21
+ onMount,
22
+ onCleanup,
23
+ batch,
24
+ Show,
25
+ on,
26
+ } from "solid-js"
27
+ import { TuiPathsProvider, TuiStartupProvider, TuiTerminalEnvironmentProvider, useTuiStartup } from "./context/runtime"
28
+ import { DialogProvider, useDialog } from "./ui/dialog"
29
+ import { DialogProvider as DialogProviderList } from "./component/dialog-provider"
30
+ import { ErrorComponent } from "./component/error-component"
31
+ import { PluginRouteMissing } from "./component/plugin-route-missing"
32
+ import { ProjectProvider, useProject } from "./context/project"
33
+ import { EditorContextProvider } from "./context/editor"
34
+ import { useEvent } from "./context/event"
35
+ import { SDKProvider, useSDK } from "./context/sdk"
36
+ import { StartupLoading } from "./component/startup-loading"
37
+ import { SyncProvider, useSync } from "./context/sync"
38
+ import { DataProvider } from "./context/data"
39
+ import { LocationProvider } from "./context/location"
40
+ import { LocalProvider, useLocal } from "./context/local"
41
+ import { PermissionProvider } from "./context/permission"
42
+ import { DialogModel } from "./component/dialog-model"
43
+ import { useConnected } from "./component/use-connected"
44
+ import { DialogMcp } from "./component/dialog-mcp"
45
+ import { DialogStatus } from "./component/dialog-status"
46
+ import { DialogDebug } from "./component/dialog-debug"
47
+ import { DialogThemeList } from "./component/dialog-theme-list"
48
+ import { DialogHelp } from "./ui/dialog-help"
49
+ import { DialogAgent } from "./component/dialog-agent"
50
+ import { DialogSessionList } from "./component/dialog-session-list"
51
+ import { DialogWorkspaceList } from "./component/dialog-workspace-list"
52
+ import { DialogConsoleOrg } from "./component/dialog-console-org"
53
+ import { ThemeProvider, useTheme } from "./context/theme"
54
+ import { Home } from "./routes/home"
55
+ import { Session } from "./routes/session"
56
+ import { PromptHistoryProvider } from "./component/prompt/history"
57
+ import { FrecencyProvider } from "./component/prompt/frecency"
58
+ import { PromptStashProvider } from "./component/prompt/stash"
59
+ import { DialogAlert } from "./ui/dialog-alert"
60
+ import { DialogConfirm } from "./ui/dialog-confirm"
61
+ import { ToastProvider, useToast } from "./ui/toast"
62
+ import { isDefaultTitle } from "./util/session"
63
+ import { KVProvider, useKV } from "./context/kv"
64
+ import * as Model from "./util/model"
65
+ import { ArgsProvider, useArgs, type Args } from "./context/args"
66
+ import open from "open"
67
+ import { PromptRefProvider, usePromptRef } from "./context/prompt"
68
+ import { TuiConfigProvider, useTuiConfig, type TuiConfig } from "./config"
69
+ import { createTuiApiAdapters } from "./plugin/adapters"
70
+ import { createTuiApi } from "./plugin/api"
71
+ import { createPluginRuntime, PluginRuntimeProvider, usePluginRuntime, type TuiPluginHost } from "./plugin/runtime"
72
+ import { CommandPaletteDialog } from "./component/command-palette"
73
+ import {
74
+ COMMAND_PALETTE_COMMAND,
75
+ NEUROCODE_BASE_MODE,
76
+ NeurocodeKeymapProvider,
77
+ registerNeurocodeKeymap,
78
+ useBindings,
79
+ useNeurocodeKeymap,
80
+ } from "./keymap"
81
+
82
+ import type { EventSource } from "./context/sdk"
83
+ import { DialogVariant } from "./component/dialog-variant"
84
+ import { createTuiAttention } from "./attention"
85
+ import * as TuiAudio from "./audio"
86
+ import { win32DisableProcessedInput, win32FlushInputBuffer } from "./terminal-win32"
87
+ import { destroyRenderer } from "./util/renderer"
88
+ import { cliErrorMessage, errorFormat } from "./util/error"
89
+
90
+ registerNeurocodeSpinner()
91
+
92
+ const appGlobalBindingCommands = [
93
+ "session.list",
94
+ "session.new",
95
+ "session.quick_switch.1",
96
+ "session.quick_switch.2",
97
+ "session.quick_switch.3",
98
+ "session.quick_switch.4",
99
+ "session.quick_switch.5",
100
+ "session.quick_switch.6",
101
+ "session.quick_switch.7",
102
+ "session.quick_switch.8",
103
+ "session.quick_switch.9",
104
+ ] as const
105
+
106
+ const appBindingCommands = [
107
+ "command.palette.show",
108
+ "model.list",
109
+ "model.cycle_recent",
110
+ "model.cycle_recent_reverse",
111
+ "model.cycle_favorite",
112
+ "model.cycle_favorite_reverse",
113
+ "agent.list",
114
+ "mcp.list",
115
+ "agent.cycle",
116
+ "agent.cycle.reverse",
117
+ "variant.cycle",
118
+ "variant.list",
119
+ "provider.connect",
120
+ "console.org.switch",
121
+ "neurocode.status",
122
+ "neurocode.debug",
123
+ "theme.switch",
124
+ "theme.switch_mode",
125
+ "theme.mode.lock",
126
+ "help.show",
127
+ "docs.open",
128
+ "diff.open",
129
+ "workspace.list",
130
+ "app.debug",
131
+ "app.console",
132
+ "app.heap_snapshot",
133
+ "terminal.suspend",
134
+ "terminal.title.toggle",
135
+ "app.toggle.animations",
136
+ "app.toggle.file_context",
137
+ "app.toggle.diffwrap",
138
+ "app.toggle.paste_summary",
139
+ "app.toggle.session_directory_filter",
140
+ ] as const
141
+
142
+ export type TuiInput = {
143
+ url: string
144
+ args: Args
145
+ config: TuiConfig.Resolved
146
+ onSnapshot?: () => Promise<string[]>
147
+ directory?: string
148
+ fetch?: typeof fetch
149
+ headers?: RequestInit["headers"]
150
+ events?: EventSource
151
+ pluginHost: TuiPluginHost
152
+ }
153
+
154
+ function errorMessage(error: unknown) {
155
+ if (
156
+ typeof error === "object" &&
157
+ error !== null &&
158
+ "data" in error &&
159
+ typeof error.data === "object" &&
160
+ error.data !== null &&
161
+ "message" in error.data &&
162
+ typeof error.data.message === "string"
163
+ ) {
164
+ return error.data.message
165
+ }
166
+ return error instanceof Error ? error.message : String(error)
167
+ }
168
+
169
+ function isVersionGreater(left: string, right: string) {
170
+ const parse = (value: string) => {
171
+ const [core, prerelease] = value.replace(/^v/, "").split("-", 2)
172
+ return { core: core.split(".").map((part) => Number.parseInt(part, 10) || 0), prerelease }
173
+ }
174
+ const a = parse(left)
175
+ const b = parse(right)
176
+ for (let index = 0; index < Math.max(a.core.length, b.core.length); index++) {
177
+ const difference = (a.core[index] ?? 0) - (b.core[index] ?? 0)
178
+ if (difference) return difference > 0
179
+ }
180
+ if (a.prerelease === b.prerelease) return false
181
+ if (!a.prerelease) return true
182
+ if (!b.prerelease) return false
183
+ return a.prerelease.localeCompare(b.prerelease, undefined, { numeric: true }) > 0
184
+ }
185
+
186
+ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
187
+ const global = yield* Global.Service
188
+ const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
189
+ const result = yield* Effect.scoped(
190
+ Effect.gen(function* () {
191
+ const renderer = yield* Effect.acquireRelease(
192
+ Effect.tryPromise({
193
+ try: () =>
194
+ createCliRenderer({
195
+ externalOutputMode: "passthrough",
196
+ targetFps: 60,
197
+ gatherStats: false,
198
+ exitOnCtrlC: false,
199
+ useKittyKeyboard: {},
200
+ autoFocus: false,
201
+ openConsoleOnError: false,
202
+ useMouse: !Flag.NEUROCODE_DISABLE_MOUSE && input.config.mouse,
203
+ consoleOptions: {
204
+ keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
205
+ },
206
+ }),
207
+ catch: (error) => (error instanceof Error ? error : new Error(String(error))),
208
+ }),
209
+ (renderer) =>
210
+ Effect.sync(() => {
211
+ destroyRenderer(renderer)
212
+ }),
213
+ )
214
+ win32DisableProcessedInput()
215
+ const keymap = createDefaultOpenTuiKeymap(renderer)
216
+ yield* Effect.acquireRelease(
217
+ Effect.sync(() => registerNeurocodeKeymap(keymap, renderer, input.config)),
218
+ (unregister) => Effect.sync(unregister),
219
+ )
220
+ yield* Effect.addFinalizer(() =>
221
+ Effect.promise(async () => {
222
+ try {
223
+ await input.pluginHost.dispose()
224
+ } catch (error) {
225
+ console.error("Failed to dispose TUI plugins", error)
226
+ }
227
+ }),
228
+ )
229
+ yield* Effect.addFinalizer(() => Effect.sync(TuiAudio.dispose))
230
+ const shutdown = yield* Deferred.make<unknown>()
231
+ const onSighup = () => destroyRenderer(renderer)
232
+ yield* Effect.acquireRelease(
233
+ Effect.sync(() => process.on("SIGHUP", onSighup)),
234
+ () => Effect.sync(() => process.off("SIGHUP", onSighup)),
235
+ )
236
+ renderer.once("destroy", () => Deferred.doneUnsafe(shutdown, Effect.void))
237
+ const pluginRuntime = createPluginRuntime()
238
+
239
+ yield* Effect.tryPromise(async () => {
240
+ // Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash.
241
+ void renderer.getPalette({ size: 16 }).catch(() => undefined)
242
+ const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
243
+ if (renderer.isDestroyed) return
244
+
245
+ await render(() => {
246
+ return (
247
+ <ExitProvider
248
+ exit={(reason) => {
249
+ if (renderer.isDestroyed) return
250
+ exit.reason = reason
251
+ destroyRenderer(renderer)
252
+ }}
253
+ >
254
+ <EpilogueProvider set={(value) => (exit.epilogue = value)}>
255
+ <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} mode={mode} />}>
256
+ <TuiPathsProvider
257
+ value={{
258
+ cwd: process.cwd(),
259
+ home: global.home,
260
+ state: global.state,
261
+ worktree: global.data + "/worktree",
262
+ }}
263
+ >
264
+ <TuiTerminalEnvironmentProvider
265
+ value={{
266
+ platform: process.platform,
267
+ multiplexer: process.env.TMUX ? "tmux" : process.env.STY ? "screen" : undefined,
268
+ displayServer: process.env.WAYLAND_DISPLAY
269
+ ? "wayland"
270
+ : process.env.DISPLAY
271
+ ? "x11"
272
+ : undefined,
273
+ }}
274
+ >
275
+ <TuiStartupProvider
276
+ value={{
277
+ initialRoute: process.env.NEUROCODE_ROUTE ? JSON.parse(process.env.NEUROCODE_ROUTE) : undefined,
278
+ skipInitialLoading: Boolean(process.env.NEUROCODE_FAST_BOOT),
279
+ }}
280
+ >
281
+ <ClipboardProvider>
282
+ <NeurocodeKeymapProvider keymap={keymap}>
283
+ <ArgsProvider {...input.args}>
284
+ <KVProvider>
285
+ <ToastProvider>
286
+ <RouteProvider
287
+ initialRoute={
288
+ input.args.continue
289
+ ? {
290
+ type: "session",
291
+ sessionID: "dummy",
292
+ }
293
+ : undefined
294
+ }
295
+ >
296
+ <TuiConfigProvider config={input.config}>
297
+ <PluginRuntimeProvider value={pluginRuntime}>
298
+ <SDKProvider
299
+ url={input.url}
300
+ directory={input.directory}
301
+ fetch={input.fetch}
302
+ headers={input.headers}
303
+ events={input.events}
304
+ >
305
+ <PermissionProvider>
306
+ <ProjectProvider>
307
+ <SyncProvider>
308
+ <DataProvider>
309
+ <ThemeProvider mode={mode}>
310
+ <LocalProvider>
311
+ <PromptStashProvider>
312
+ <DialogProvider>
313
+ <FrecencyProvider>
314
+ <PromptHistoryProvider>
315
+ <PromptRefProvider>
316
+ <EditorContextProvider>
317
+ <LocationProvider>
318
+ <App
319
+ onSnapshot={input.onSnapshot}
320
+ pluginHost={input.pluginHost}
321
+ />
322
+ </LocationProvider>
323
+ </EditorContextProvider>
324
+ </PromptRefProvider>
325
+ </PromptHistoryProvider>
326
+ </FrecencyProvider>
327
+ </DialogProvider>
328
+ </PromptStashProvider>
329
+ </LocalProvider>
330
+ </ThemeProvider>
331
+ </DataProvider>
332
+ </SyncProvider>
333
+ </ProjectProvider>
334
+ </PermissionProvider>
335
+ </SDKProvider>
336
+ </PluginRuntimeProvider>
337
+ </TuiConfigProvider>
338
+ </RouteProvider>
339
+ </ToastProvider>
340
+ </KVProvider>
341
+ </ArgsProvider>
342
+ </NeurocodeKeymapProvider>
343
+ </ClipboardProvider>
344
+ </TuiStartupProvider>
345
+ </TuiTerminalEnvironmentProvider>
346
+ </TuiPathsProvider>
347
+ </ErrorBoundary>
348
+ </EpilogueProvider>
349
+ </ExitProvider>
350
+ )
351
+ }, renderer)
352
+ })
353
+ yield* Deferred.await(shutdown)
354
+ return { epilogue: exit.epilogue, reason: exit.reason }
355
+ }),
356
+ )
357
+ yield* Effect.sync(() => {
358
+ win32FlushInputBuffer()
359
+ if (result.reason !== undefined)
360
+ process.stderr.write((cliErrorMessage(result.reason) ?? errorFormat(result.reason)) + "\n")
361
+ if (result.epilogue) process.stdout.write(result.epilogue + "\n")
362
+ })
363
+ })
364
+
365
+ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPluginHost }) {
366
+ const startup = useTuiStartup()
367
+ const tuiConfig = useTuiConfig()
368
+ const route = useRoute()
369
+ const dimensions = useTerminalDimensions()
370
+ const renderer = useRenderer()
371
+ const dialog = useDialog()
372
+ const local = useLocal()
373
+ const kv = useKV()
374
+ const keymap = useNeurocodeKeymap()
375
+ const event = useEvent()
376
+ const sdk = useSDK()
377
+ const toast = useToast()
378
+ const themeState = useTheme()
379
+ const { theme, mode, setMode, locked, lock, unlock } = themeState
380
+ const sync = useSync()
381
+ const project = useProject()
382
+ const exit = useExit()
383
+ const promptRef = usePromptRef()
384
+ const pluginRuntime = usePluginRuntime()
385
+ const attention = createTuiAttention({ renderer, config: tuiConfig, kv })
386
+ const clipboard = useClipboard()
387
+
388
+ const api = createTuiApi(
389
+ createTuiApiAdapters({
390
+ version: InstallationVersion,
391
+ tuiConfig,
392
+ dialog,
393
+ keymap,
394
+ kv,
395
+ route,
396
+ routes: pluginRuntime.routes,
397
+ event,
398
+ sdk,
399
+ sync,
400
+ theme: themeState,
401
+ toast,
402
+ renderer,
403
+ attention,
404
+ Slot: pluginRuntime.Slot,
405
+ }),
406
+ )
407
+ const [ready, setReady] = createSignal(false)
408
+ props.pluginHost
409
+ .start({
410
+ api,
411
+ config: tuiConfig,
412
+ runtime: pluginRuntime,
413
+ dispose: () => attention.dispose(),
414
+ })
415
+ .catch((error) => {
416
+ console.error("Failed to load TUI plugins", error)
417
+ })
418
+ .finally(() => {
419
+ setReady(true)
420
+ })
421
+
422
+ // Let selection copy/dismiss win ahead of normal bindings when explicit copy is required.
423
+ const offSelectionKeys = keymap.intercept(
424
+ "key",
425
+ ({ event }) => {
426
+ if (!Flag.NEUROCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
427
+ Selection.handleSelectionKey(renderer, toast, event, clipboard)
428
+ },
429
+ { priority: 1 },
430
+ )
431
+ onCleanup(() => {
432
+ offSelectionKeys()
433
+ attention.dispose()
434
+ })
435
+
436
+ // Wire up console copy-to-clipboard via opentui's onCopySelection callback
437
+ renderer.console.onCopySelection = async (text: string) => {
438
+ if (!text || text.length === 0) return
439
+
440
+ await clipboard
441
+ .write?.(text)
442
+ .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
443
+ .catch(toast.error)
444
+
445
+ renderer.clearSelection()
446
+ }
447
+ const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
448
+ const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(
449
+ kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary),
450
+ )
451
+
452
+ // Update terminal window title based on current route and session
453
+ createEffect(() => {
454
+ if (!terminalTitleEnabled() || Flag.NEUROCODE_DISABLE_TERMINAL_TITLE) return
455
+
456
+ if (route.data.type === "home") {
457
+ renderer.setTerminalTitle("NeuroCode")
458
+ return
459
+ }
460
+
461
+ if (route.data.type === "session") {
462
+ const session = sync.session.get(route.data.sessionID)
463
+ if (!session || isDefaultTitle(session.title)) {
464
+ renderer.setTerminalTitle("NeuroCode")
465
+ return
466
+ }
467
+
468
+ const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
469
+ renderer.setTerminalTitle(`NC | ${title}`)
470
+ return
471
+ }
472
+
473
+ if (route.data.type === "plugin") {
474
+ renderer.setTerminalTitle(`NC | ${route.data.id}`)
475
+ }
476
+ })
477
+
478
+ const args = useArgs()
479
+ onMount(() => {
480
+ batch(() => {
481
+ if (args.agent) local.agent.set(args.agent)
482
+ if (args.model) {
483
+ const { providerID, modelID } = Model.parse(args.model)
484
+ if (!providerID || !modelID)
485
+ return toast.show({
486
+ variant: "warning",
487
+ message: `Invalid model format: ${args.model}`,
488
+ duration: 3000,
489
+ })
490
+ local.model.set({ providerID, modelID }, { recent: true })
491
+ }
492
+ if (args.sessionID && !args.fork) {
493
+ route.navigate({
494
+ type: "session",
495
+ sessionID: args.sessionID,
496
+ })
497
+ }
498
+ })
499
+ })
500
+
501
+ let continued = false
502
+ createEffect(() => {
503
+ // When using -c, session list is loaded in blocking phase, so we can navigate at "partial"
504
+ if (continued || sync.status === "loading" || !args.continue) return
505
+ const match = sync.data.session
506
+ .toSorted((a, b) => b.time.updated - a.time.updated)
507
+ .find((x) => x.parentID === undefined)?.id
508
+ if (match) {
509
+ continued = true
510
+ if (args.fork) {
511
+ void sdk.client.session.fork({ sessionID: match }).then((result) => {
512
+ if (result.data?.id) {
513
+ route.navigate({ type: "session", sessionID: result.data.id })
514
+ } else {
515
+ toast.show({ message: "Failed to fork session", variant: "error" })
516
+ }
517
+ })
518
+ } else {
519
+ route.navigate({ type: "session", sessionID: match })
520
+ }
521
+ }
522
+ })
523
+
524
+ // Handle --session with --fork: wait for sync to be fully complete before forking
525
+ // (session list loads in non-blocking phase for --session, so we must wait for "complete"
526
+ // to avoid a race where reconcile overwrites the newly forked session)
527
+ let forked = false
528
+ createEffect(() => {
529
+ if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
530
+ forked = true
531
+ void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
532
+ if (result.data?.id) {
533
+ route.navigate({ type: "session", sessionID: result.data.id })
534
+ } else {
535
+ toast.show({ message: "Failed to fork session", variant: "error" })
536
+ }
537
+ })
538
+ })
539
+
540
+ createEffect(
541
+ on(
542
+ () => sync.status === "complete" && sync.data.provider.length === 0,
543
+ (isEmpty, wasEmpty) => {
544
+ // only trigger when we transition into an empty-provider state
545
+ if (!isEmpty || wasEmpty) return
546
+ dialog.replace(() => <DialogProviderList />)
547
+ },
548
+ ),
549
+ )
550
+
551
+ const connected = useConnected()
552
+ const currentWorktreeWorkspace = createMemo(() => {
553
+ const workspaceID = project.workspace.current()
554
+ if (!workspaceID) return
555
+ const workspace = project.workspace.get(workspaceID)
556
+ if (workspace?.type !== "worktree" || !workspace.directory) return
557
+ return workspace
558
+ })
559
+ const appCommands = createMemo(() =>
560
+ [
561
+ {
562
+ name: COMMAND_PALETTE_COMMAND,
563
+ title: "Show command palette",
564
+ category: "System",
565
+ hidden: true,
566
+ run: () => {
567
+ dialog.replace(() => <CommandPaletteDialog />)
568
+ },
569
+ },
570
+ {
571
+ name: "session.list",
572
+ title: "Switch session",
573
+ category: "Session",
574
+ suggested: sync.data.session.length > 0,
575
+ slashName: "sessions",
576
+ slashAliases: ["resume", "continue"],
577
+ run: () => {
578
+ dialog.replace(() => <DialogSessionList />)
579
+ },
580
+ },
581
+ {
582
+ name: "session.new",
583
+ title: "New session",
584
+ suggested: route.data.type === "session",
585
+ category: "Session",
586
+ slashName: "new",
587
+ slashAliases: ["clear"],
588
+ run: () => {
589
+ route.navigate({
590
+ type: "home",
591
+ })
592
+ dialog.clear()
593
+ },
594
+ },
595
+ {
596
+ name: "workspace.copy_path",
597
+ title: "Copy worktree path",
598
+ category: "Workspace",
599
+ enabled: () => currentWorktreeWorkspace() !== undefined,
600
+ run: async () => {
601
+ const workspace = currentWorktreeWorkspace()
602
+ if (!workspace?.directory) return
603
+ await clipboard
604
+ .write?.(workspace.directory)
605
+ .then(() => toast.show({ message: "Copied worktree path", variant: "info" }))
606
+ .catch(toast.error)
607
+ dialog.clear()
608
+ },
609
+ },
610
+ {
611
+ name: "workspace.list",
612
+ title: "Manage workspaces",
613
+ category: "Workspace",
614
+ hidden: !Flag.NEUROCODE_EXPERIMENTAL_WORKSPACES,
615
+ slashName: "workspaces",
616
+ run: () => {
617
+ dialog.replace(() => <DialogWorkspaceList />)
618
+ },
619
+ },
620
+ ...Array.from({ length: 9 }, (_, i) => ({
621
+ name: `session.quick_switch.${i + 1}`,
622
+ title: `Switch to session in quick slot ${i + 1}`,
623
+ category: "Session",
624
+ hidden: true,
625
+ run: () => {
626
+ local.session.quickSwitch(i + 1)
627
+ },
628
+ })),
629
+ {
630
+ name: "model.list",
631
+ title: "Switch model",
632
+ suggested: true,
633
+ category: "Agent",
634
+ slashName: "models",
635
+ // Bias /mo toward /models over /move without changing global fuzzy scoring.
636
+ slashAliases: ["mo"],
637
+ run: () => {
638
+ dialog.replace(() => <DialogModel />)
639
+ },
640
+ },
641
+ {
642
+ name: "model.cycle_recent",
643
+ title: "Model cycle",
644
+ category: "Agent",
645
+ hidden: true,
646
+ run: () => {
647
+ local.model.cycle(1)
648
+ },
649
+ },
650
+ {
651
+ name: "model.cycle_recent_reverse",
652
+ title: "Model cycle reverse",
653
+ category: "Agent",
654
+ hidden: true,
655
+ run: () => {
656
+ local.model.cycle(-1)
657
+ },
658
+ },
659
+ {
660
+ name: "model.cycle_favorite",
661
+ title: "Favorite cycle",
662
+ category: "Agent",
663
+ hidden: true,
664
+ run: () => {
665
+ local.model.cycleFavorite(1)
666
+ },
667
+ },
668
+ {
669
+ name: "model.cycle_favorite_reverse",
670
+ title: "Favorite cycle reverse",
671
+ category: "Agent",
672
+ hidden: true,
673
+ run: () => {
674
+ local.model.cycleFavorite(-1)
675
+ },
676
+ },
677
+ {
678
+ name: "agent.list",
679
+ title: "Switch agent",
680
+ category: "Agent",
681
+ slashName: "agents",
682
+ run: () => {
683
+ dialog.replace(() => <DialogAgent />)
684
+ },
685
+ },
686
+ {
687
+ name: "mcp.list",
688
+ title: "Toggle MCPs",
689
+ category: "Agent",
690
+ slashName: "mcps",
691
+ run: () => {
692
+ dialog.replace(() => <DialogMcp />)
693
+ },
694
+ },
695
+ {
696
+ name: "agent.cycle",
697
+ title: "Agent cycle",
698
+ category: "Agent",
699
+ hidden: true,
700
+ run: () => {
701
+ local.agent.move(1)
702
+ },
703
+ },
704
+ {
705
+ name: "variant.cycle",
706
+ title: "Variant cycle",
707
+ category: "Agent",
708
+ run: () => {
709
+ local.model.variant.cycle()
710
+ },
711
+ },
712
+ {
713
+ name: "variant.list",
714
+ title: "Switch model variant",
715
+ category: "Agent",
716
+ hidden: local.model.variant.list().length === 0,
717
+ slashName: "variants",
718
+ run: () => {
719
+ if (local.model.variant.list().length === 0) {
720
+ return toast.show({
721
+ title: "No variants available",
722
+ message: "The current model does not support any variants.",
723
+ variant: "info",
724
+ })
725
+ }
726
+ dialog.replace(() => <DialogVariant />)
727
+ },
728
+ },
729
+ {
730
+ name: "agent.cycle.reverse",
731
+ title: "Agent cycle reverse",
732
+ category: "Agent",
733
+ hidden: true,
734
+ run: () => {
735
+ local.agent.move(-1)
736
+ },
737
+ },
738
+ {
739
+ name: "provider.connect",
740
+ title: "Connect provider",
741
+ suggested: !connected(),
742
+ slashName: "connect",
743
+ run: () => {
744
+ dialog.replace(() => <DialogProviderList />)
745
+ },
746
+ category: "Provider",
747
+ },
748
+ ...(sync.data.console_state.switchableOrgCount > 1
749
+ ? [
750
+ {
751
+ name: "console.org.switch",
752
+ title: "Switch org",
753
+ suggested: Boolean(sync.data.console_state.activeOrgName),
754
+ slashName: "org",
755
+ slashAliases: ["orgs", "switch-org"],
756
+ run: () => {
757
+ dialog.replace(() => <DialogConsoleOrg />)
758
+ },
759
+ category: "Provider",
760
+ },
761
+ ]
762
+ : []),
763
+ {
764
+ name: "opencode.status",
765
+ title: "View status",
766
+ slashName: "status",
767
+ run: () => {
768
+ dialog.replace(() => <DialogStatus />)
769
+ },
770
+ category: "System",
771
+ },
772
+ {
773
+ name: "opencode.debug",
774
+ title: "View debug info",
775
+ slashName: "debug",
776
+ run: () => {
777
+ dialog.replace(() => <DialogDebug />)
778
+ },
779
+ category: "System",
780
+ },
781
+ {
782
+ name: "theme.switch",
783
+ title: "Switch theme",
784
+ slashName: "themes",
785
+ run: () => {
786
+ dialog.replace(() => <DialogThemeList />)
787
+ },
788
+ category: "System",
789
+ },
790
+ {
791
+ name: "theme.switch_mode",
792
+ title: mode() === "dark" ? "Switch to light mode" : "Switch to dark mode",
793
+ run: () => {
794
+ setMode(mode() === "dark" ? "light" : "dark")
795
+ dialog.clear()
796
+ },
797
+ category: "System",
798
+ },
799
+ {
800
+ name: "theme.mode.lock",
801
+ title: locked() ? "Unlock theme mode" : "Lock theme mode",
802
+ run: () => {
803
+ if (locked()) unlock()
804
+ else lock()
805
+ dialog.clear()
806
+ },
807
+ category: "System",
808
+ },
809
+ {
810
+ name: "help.show",
811
+ title: "Help",
812
+ slashName: "help",
813
+ run: () => {
814
+ dialog.replace(() => <DialogHelp />)
815
+ },
816
+ category: "System",
817
+ },
818
+ {
819
+ name: "docs.open",
820
+ title: "Open docs",
821
+ run: () => {
822
+ open("https://opencode.ai/docs").catch(() => {})
823
+ dialog.clear()
824
+ },
825
+ category: "System",
826
+ },
827
+ {
828
+ name: "app.exit",
829
+ title: "Exit the app",
830
+ slashName: "exit",
831
+ slashAliases: ["quit", "q"],
832
+ run: () => exit(),
833
+ category: "System",
834
+ },
835
+ {
836
+ name: "app.debug",
837
+ title: "Toggle debug panel",
838
+ category: "System",
839
+ run: () => {
840
+ renderer.toggleDebugOverlay()
841
+ dialog.clear()
842
+ },
843
+ },
844
+ {
845
+ name: "app.console",
846
+ title: "Toggle console",
847
+ category: "System",
848
+ run: () => {
849
+ renderer.console.toggle()
850
+ dialog.clear()
851
+ },
852
+ },
853
+ {
854
+ name: "app.heap_snapshot",
855
+ title: "Write heap snapshot",
856
+ category: "System",
857
+ run: async () => {
858
+ const files = await props.onSnapshot?.()
859
+ toast.show({
860
+ variant: "info",
861
+ message: `Heap snapshot written to ${files?.join(", ")}`,
862
+ duration: 5000,
863
+ })
864
+ dialog.clear()
865
+ },
866
+ },
867
+ {
868
+ name: "terminal.suspend",
869
+ title: "Suspend terminal",
870
+ category: "System",
871
+ hidden: true,
872
+ enabled: process.platform !== "win32",
873
+ run: () => {
874
+ renderer.suspend()
875
+ process.once("SIGCONT", () => renderer.resume())
876
+ process.kill(0, "SIGTSTP")
877
+ },
878
+ },
879
+ {
880
+ name: "terminal.title.toggle",
881
+ title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title",
882
+ category: "System",
883
+ run: () => {
884
+ setTerminalTitleEnabled((prev) => {
885
+ const next = !prev
886
+ kv.set("terminal_title_enabled", next)
887
+ if (!next) renderer.setTerminalTitle("")
888
+ return next
889
+ })
890
+ dialog.clear()
891
+ },
892
+ },
893
+ {
894
+ name: "app.toggle.animations",
895
+ title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations",
896
+ category: "System",
897
+ run: () => {
898
+ kv.set("animations_enabled", !kv.get("animations_enabled", true))
899
+ dialog.clear()
900
+ },
901
+ },
902
+ {
903
+ name: "app.toggle.file_context",
904
+ title: kv.get("file_context_enabled", true) ? "Disable file context" : "Enable file context",
905
+ category: "System",
906
+ run: () => {
907
+ kv.set("file_context_enabled", !kv.get("file_context_enabled", true))
908
+ dialog.clear()
909
+ },
910
+ },
911
+ {
912
+ name: "app.toggle.diffwrap",
913
+ title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
914
+ category: "System",
915
+ run: () => {
916
+ const current = kv.get("diff_wrap_mode", "word")
917
+ kv.set("diff_wrap_mode", current === "word" ? "none" : "word")
918
+ dialog.clear()
919
+ },
920
+ },
921
+ {
922
+ name: "app.toggle.paste_summary",
923
+ title: pasteSummaryEnabled() ? "Disable paste summary" : "Enable paste summary",
924
+ category: "System",
925
+ run: () => {
926
+ setPasteSummaryEnabled((prev) => {
927
+ const next = !prev
928
+ kv.set("paste_summary_enabled", next)
929
+ return next
930
+ })
931
+ dialog.clear()
932
+ },
933
+ },
934
+ {
935
+ name: "app.toggle.session_directory_filter",
936
+ title: kv.get("session_directory_filter_enabled", true)
937
+ ? "Disable session directory filtering"
938
+ : "Enable session directory filtering",
939
+ category: "System",
940
+ run: async () => {
941
+ kv.set("session_directory_filter_enabled", !kv.get("session_directory_filter_enabled", true))
942
+ await sync.session.refresh()
943
+ dialog.clear()
944
+ },
945
+ },
946
+ {
947
+ name: "permission.mode",
948
+ title:
949
+ local.permission.mode === "auto" ? "Disable auto-approve permissions" : "Enable auto-approve permissions",
950
+ category: "System",
951
+ run: () => {
952
+ local.permission.toggle()
953
+ dialog.clear()
954
+ },
955
+ },
956
+ ].map((command) => ({
957
+ namespace: "palette",
958
+ ...command,
959
+ })),
960
+ )
961
+
962
+ useBindings(() => ({
963
+ commands: appCommands(),
964
+ }))
965
+
966
+ useBindings(() => ({
967
+ mode: NEUROCODE_BASE_MODE,
968
+ bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
969
+ }))
970
+
971
+ useBindings(() => ({
972
+ bindings: tuiConfig.keybinds.gather("app.global", appGlobalBindingCommands),
973
+ }))
974
+
975
+ useBindings(() => ({
976
+ mode: NEUROCODE_BASE_MODE,
977
+ enabled: () => {
978
+ const current = promptRef.current
979
+ if (!current?.focused) return true
980
+ return current.current.input === ""
981
+ },
982
+ bindings: tuiConfig.keybinds.gather("app_exit", ["app.exit"]),
983
+ }))
984
+
985
+ event.on("tui.command.execute", (evt, { workspace }) => {
986
+ if (workspace !== project.workspace.current()) return
987
+ keymap.dispatchCommand(evt.properties.command)
988
+ })
989
+
990
+ event.on("tui.toast.show", (evt, { workspace }) => {
991
+ if (workspace !== project.workspace.current()) return
992
+ toast.show({
993
+ title: evt.properties.title,
994
+ message: evt.properties.message,
995
+ variant: evt.properties.variant,
996
+ duration: evt.properties.duration,
997
+ })
998
+ })
999
+
1000
+ event.on("tui.session.select", (evt, { workspace }) => {
1001
+ if (workspace !== project.workspace.current()) return
1002
+ route.navigate({
1003
+ type: "session",
1004
+ sessionID: evt.properties.sessionID,
1005
+ })
1006
+ })
1007
+
1008
+ event.on("session.deleted", (evt) => {
1009
+ if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
1010
+ route.navigate({ type: "home" })
1011
+ toast.show({
1012
+ variant: "info",
1013
+ message: "The current session was deleted",
1014
+ })
1015
+ }
1016
+ })
1017
+
1018
+ event.on("session.error", (evt, { workspace }) => {
1019
+ if (workspace !== project.workspace.current()) return
1020
+ const error = evt.properties.error
1021
+ if (error && typeof error === "object" && error.name === "MessageAbortedError") return
1022
+ const message = errorMessage(error)
1023
+
1024
+ toast.show({
1025
+ variant: "error",
1026
+ message,
1027
+ duration: 5000,
1028
+ })
1029
+ })
1030
+
1031
+ event.on("installation.update-available", async (evt) => {
1032
+ console.log("installation.update-available", evt)
1033
+ const version = evt.properties.version
1034
+
1035
+ const skipped = kv.get("skipped_version")
1036
+ if (skipped && !isVersionGreater(version, skipped)) return
1037
+
1038
+ const choice = await DialogConfirm.show(
1039
+ dialog,
1040
+ `Update Available`,
1041
+ `A new release v${version} is available. Would you like to update now?`,
1042
+ "skip",
1043
+ )
1044
+
1045
+ if (choice === false) {
1046
+ kv.set("skipped_version", version)
1047
+ return
1048
+ }
1049
+
1050
+ if (choice !== true) return
1051
+
1052
+ toast.show({
1053
+ variant: "info",
1054
+ message: `Updating to v${version}...`,
1055
+ duration: 30000,
1056
+ })
1057
+
1058
+ const result = await sdk.client.global.upgrade({ target: version })
1059
+
1060
+ if (result.error || !result.data?.success) {
1061
+ toast.show({
1062
+ variant: "error",
1063
+ title: "Update Failed",
1064
+ message: "Update failed",
1065
+ duration: 10000,
1066
+ })
1067
+ return
1068
+ }
1069
+
1070
+ await DialogAlert.show(
1071
+ dialog,
1072
+ "Update Complete",
1073
+ `Successfully updated to NeuroCode v${result.data.version}. Please restart the application.`,
1074
+ )
1075
+
1076
+ void exit()
1077
+ })
1078
+
1079
+ const plugin = createMemo(() => {
1080
+ if (!ready()) return
1081
+ if (route.data.type !== "plugin") return
1082
+ const render = pluginRuntime.routes.get(route.data.id)
1083
+ if (!render) return <PluginRouteMissing id={route.data.id} onHome={() => route.navigate({ type: "home" })} />
1084
+ return render({ params: route.data.data })
1085
+ })
1086
+
1087
+ return (
1088
+ <box
1089
+ width={dimensions().width}
1090
+ height={dimensions().height}
1091
+ flexDirection="column"
1092
+ backgroundColor={theme.background}
1093
+ onMouseDown={(evt) => {
1094
+ if (!Flag.NEUROCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
1095
+ if (evt.button !== MouseButton.RIGHT) return
1096
+
1097
+ if (!Selection.copy(renderer, toast, clipboard)) return
1098
+ evt.preventDefault()
1099
+ evt.stopPropagation()
1100
+ }}
1101
+ onMouseUp={
1102
+ !Flag.NEUROCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
1103
+ ? () => Selection.copy(renderer, toast, clipboard)
1104
+ : undefined
1105
+ }
1106
+ >
1107
+ <Show when={Flag.NEUROCODE_SHOW_TTFD}>
1108
+ <TimeToFirstDraw />
1109
+ </Show>
1110
+ <Show when={ready()}>
1111
+ <box flexGrow={1} minHeight={0} flexDirection="column">
1112
+ <Switch>
1113
+ <Match when={route.data.type === "home"}>
1114
+ <Home />
1115
+ </Match>
1116
+ <Match when={route.data.type === "session"}>
1117
+ <Show when={route.data.type === "session" ? route.data.sessionID : undefined} keyed>
1118
+ {(_) => <Session />}
1119
+ </Show>
1120
+ </Match>
1121
+ </Switch>
1122
+ {plugin()}
1123
+ </box>
1124
+ <box flexShrink={0}>
1125
+ <pluginRuntime.Slot name="app_bottom" />
1126
+ </box>
1127
+ <pluginRuntime.Slot name="app" />
1128
+ </Show>
1129
+ <Show when={!startup.skipInitialLoading}>
1130
+ <StartupLoading ready={ready} />
1131
+ </Show>
1132
+ </box>
1133
+ )
1134
+ }