@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,783 @@
1
+ import { render, TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid"
2
+ import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
3
+ import * as Clipboard from "@tui/util/clipboard"
4
+ import * as Selection from "@tui/util/selection"
5
+ import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
6
+ import { RouteProvider, useRoute } from "@tui/context/route"
7
+ import {
8
+ Switch,
9
+ Match,
10
+ createEffect,
11
+ createMemo,
12
+ ErrorBoundary,
13
+ createSignal,
14
+ onMount,
15
+ onCleanup,
16
+ batch,
17
+ Show,
18
+ on,
19
+ } from "solid-js"
20
+ import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
21
+ import { Flag } from "@opencode-ai/core/flag/flag"
22
+ import { DialogProvider, useDialog } from "@tui/ui/dialog"
23
+ import { ErrorComponent } from "@tui/component/error-component"
24
+ import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
25
+ import { ProjectProvider } from "@tui/context/project-apx"
26
+ import { EditorContextProvider } from "@tui/context/editor"
27
+ import { useEvent } from "@tui/context/event-apx"
28
+ import { SDKProvider, useSDK } from "@tui/context/sdk-apx"
29
+ import { StartupLoading } from "@tui/component/startup-loading"
30
+ import { ApxSyncProvider, useApxSync } from "@tui/context/sync-apx"
31
+ import { SyncProvider } from "@tui/context/sync"
32
+ import { LocalProvider, useLocal } from "@tui/context/local"
33
+ import { DialogModel } from "@tui/component/dialog-model"
34
+ import { useConnected } from "@tui/component/use-connected"
35
+ import { DialogMcp } from "@tui/component/dialog-mcp"
36
+ import { DialogStatus } from "@tui/component/dialog-status"
37
+ import { DialogThemeList } from "@tui/component/dialog-theme-list"
38
+ import { DialogHelp } from "./ui/dialog-help"
39
+ import { DialogAgent } from "@tui/component/dialog-agent"
40
+ import { DialogSessionList } from "@tui/component/dialog-session-list"
41
+ import { ThemeProvider, useTheme } from "@tui/context/theme"
42
+ import { Home } from "@tui/routes/home"
43
+ import { Session } from "@tui/routes/session"
44
+ import { PromptHistoryProvider } from "./component/prompt/history"
45
+ import { FrecencyProvider } from "./component/prompt/frecency"
46
+ import { PromptStashProvider } from "./component/prompt/stash"
47
+ import { DialogAlert } from "./ui/dialog-alert"
48
+ import { ToastProvider, useToast } from "./ui/toast"
49
+ import { ExitProvider, useExit } from "./context/exit"
50
+ import { TuiEvent } from "./event"
51
+ import { KVProvider, useKV } from "./context/kv"
52
+ import { ArgsProvider, useArgs, type Args } from "./context/args"
53
+ import { PromptRefProvider, usePromptRef } from "./context/prompt"
54
+ import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
55
+ import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
56
+ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
57
+ import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
58
+ import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
59
+ import { FormatError, FormatUnknownError } from "@/cli/error"
60
+ import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette"
61
+ import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap"
62
+ import { DialogVariant } from "./component/dialog-variant"
63
+
64
+ const appBindingCommands = [
65
+ "command.palette.show",
66
+ "session.list",
67
+ "session.new",
68
+ "session.cycle_recent",
69
+ "session.cycle_recent_reverse",
70
+ "session.quick_switch.1",
71
+ "session.quick_switch.2",
72
+ "session.quick_switch.3",
73
+ "session.quick_switch.4",
74
+ "session.quick_switch.5",
75
+ "session.quick_switch.6",
76
+ "session.quick_switch.7",
77
+ "session.quick_switch.8",
78
+ "session.quick_switch.9",
79
+ "model.list",
80
+ "model.cycle_recent",
81
+ "model.cycle_recent_reverse",
82
+ "model.cycle_favorite",
83
+ "model.cycle_favorite_reverse",
84
+ "agent.list",
85
+ "mcp.list",
86
+ "agent.cycle",
87
+ "agent.cycle.reverse",
88
+ "variant.cycle",
89
+ "variant.list",
90
+ "opencode.status",
91
+ "theme.switch",
92
+ "theme.switch_mode",
93
+ "theme.mode.lock",
94
+ "help.show",
95
+ "docs.open",
96
+ "app.debug",
97
+ "app.console",
98
+ "app.heap_snapshot",
99
+ "terminal.suspend",
100
+ "terminal.title.toggle",
101
+ "app.toggle.animations",
102
+ "app.toggle.file_context",
103
+ "app.toggle.diffwrap",
104
+ "app.toggle.paste_summary",
105
+ "app.toggle.session_directory_filter",
106
+ ] as const
107
+
108
+ function rendererConfig(_config: TuiConfig.Resolved): CliRendererConfig {
109
+ const mouseEnabled = !Flag.OPENCODE_DISABLE_MOUSE && (_config.mouse ?? true)
110
+
111
+ return {
112
+ externalOutputMode: "passthrough",
113
+ targetFps: 60,
114
+ gatherStats: false,
115
+ exitOnCtrlC: false,
116
+ useKittyKeyboard: {},
117
+ autoFocus: false,
118
+ openConsoleOnError: false,
119
+ useMouse: mouseEnabled,
120
+ consoleOptions: {
121
+ keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
122
+ onCopySelection: (text) => {
123
+ Clipboard.copy(text).catch((error) => {
124
+ console.error(`Failed to copy console selection to clipboard: ${error}`)
125
+ })
126
+ },
127
+ },
128
+ }
129
+ }
130
+
131
+ function errorMessage(error: unknown) {
132
+ const formatted = FormatError(error)
133
+ if (formatted !== undefined) return formatted
134
+ if (
135
+ typeof error === "object" &&
136
+ error !== null &&
137
+ "data" in error &&
138
+ typeof (error as any).data === "object" &&
139
+ (error as any).data !== null &&
140
+ "message" in (error as any).data &&
141
+ typeof (error as any).data.message === "string"
142
+ ) {
143
+ return (error as any).data.message
144
+ }
145
+ return FormatUnknownError(error)
146
+ }
147
+
148
+ export function tui(input: {
149
+ url: string
150
+ pid: string
151
+ agent?: string
152
+ model?: string
153
+ args: Args
154
+ config: TuiConfig.Resolved
155
+ onSnapshot?: () => Promise<string[]>
156
+ directory?: string
157
+ fetch?: typeof fetch
158
+ headers?: RequestInit["headers"]
159
+ events?: unknown
160
+ }) {
161
+ // promise to prevent immediate exit
162
+ // oxlint-disable-next-line no-async-promise-executor -- intentional: async executor used for sequential setup before resolve
163
+ return new Promise<void>(async (resolve) => {
164
+ const unguard = win32InstallCtrlCGuard()
165
+ win32DisableProcessedInput()
166
+
167
+ const onExit = async () => {
168
+ unguard?.()
169
+ resolve()
170
+ }
171
+
172
+ const onBeforeExit = async () => {
173
+ offKeymap()
174
+ await TuiPluginRuntime.dispose()
175
+ }
176
+
177
+ const renderer = await createCliRenderer(rendererConfig(input.config))
178
+ // Prewarm palette before ThemeProvider mounts so `system` theme avoids a first-paint fallback flash.
179
+ void renderer.getPalette({ size: 16 }).catch(() => undefined)
180
+ const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
181
+
182
+ const keymap = createDefaultOpenTuiKeymap(renderer)
183
+ const offKeymap = registerOpencodeKeymap(keymap, renderer, input.config)
184
+
185
+ await render(() => {
186
+ return (
187
+ <ErrorBoundary
188
+ fallback={(error, reset) => (
189
+ <ErrorComponent error={error} reset={reset} onBeforeExit={onBeforeExit} onExit={onExit} mode={mode} />
190
+ )}
191
+ >
192
+ <OpencodeKeymapProvider keymap={keymap}>
193
+ <ArgsProvider {...input.args}>
194
+ <ExitProvider onBeforeExit={onBeforeExit} onExit={onExit}>
195
+ <KVProvider>
196
+ <ToastProvider>
197
+ <RouteProvider
198
+ initialRoute={
199
+ input.args.continue
200
+ ? {
201
+ type: "session",
202
+ sessionID: "dummy",
203
+ }
204
+ : undefined
205
+ }
206
+ >
207
+ <TuiConfigProvider config={input.config}>
208
+ <SDKProvider
209
+ url={input.url}
210
+ pid={input.pid}
211
+ agent={input.agent}
212
+ model={input.model}
213
+ directory={input.directory}
214
+ fetch={input.fetch}
215
+ headers={input.headers}
216
+ events={input.events}
217
+ >
218
+ <ProjectProvider>
219
+ <ApxSyncProvider>
220
+ <SyncProvider>
221
+ <ThemeProvider mode={mode}>
222
+ <LocalProvider>
223
+ <PromptStashProvider>
224
+ <DialogProvider>
225
+ <CommandPaletteProvider>
226
+ <FrecencyProvider>
227
+ <PromptHistoryProvider>
228
+ <PromptRefProvider>
229
+ <EditorContextProvider>
230
+ <App onSnapshot={input.onSnapshot} />
231
+ </EditorContextProvider>
232
+ </PromptRefProvider>
233
+ </PromptHistoryProvider>
234
+ </FrecencyProvider>
235
+ </CommandPaletteProvider>
236
+ </DialogProvider>
237
+ </PromptStashProvider>
238
+ </LocalProvider>
239
+ </ThemeProvider>
240
+ </SyncProvider>
241
+ </ApxSyncProvider>
242
+ </ProjectProvider>
243
+ </SDKProvider>
244
+ </TuiConfigProvider>
245
+ </RouteProvider>
246
+ </ToastProvider>
247
+ </KVProvider>
248
+ </ExitProvider>
249
+ </ArgsProvider>
250
+ </OpencodeKeymapProvider>
251
+ </ErrorBoundary>
252
+ )
253
+ }, renderer)
254
+ })
255
+ }
256
+
257
+ function App(props: { onSnapshot?: () => Promise<string[]> }) {
258
+ const tuiConfig = useTuiConfig()
259
+ const route = useRoute()
260
+ const dimensions = useTerminalDimensions()
261
+ const renderer = useRenderer()
262
+ const dialog = useDialog()
263
+ const local = useLocal()
264
+ const kv = useKV()
265
+ const command = useCommandPalette()
266
+ const keymap = useOpencodeKeymap()
267
+ const event = useEvent()
268
+ const sdk = useSDK()
269
+ const toast = useToast()
270
+ const themeState = useTheme()
271
+ const { theme, mode, setMode, locked, lock, unlock } = themeState
272
+ const sync = useApxSync()
273
+ const exit = useExit()
274
+ const promptRef = usePromptRef()
275
+ const routes: RouteMap = new Map()
276
+ const [routeRev, setRouteRev] = createSignal(0)
277
+ const routeView = (name: string) => {
278
+ routeRev()
279
+ return routes.get(name)?.at(-1)?.render
280
+ }
281
+
282
+ const api = createTuiApi({
283
+ tuiConfig,
284
+ dialog,
285
+ keymap,
286
+ kv,
287
+ route,
288
+ routes,
289
+ bump: () => setRouteRev((x) => x + 1),
290
+ event,
291
+ sdk,
292
+ sync,
293
+ theme: themeState,
294
+ toast,
295
+ renderer,
296
+ })
297
+ const [ready, setReady] = createSignal(false)
298
+ TuiPluginRuntime.init({
299
+ api,
300
+ config: tuiConfig,
301
+ })
302
+ .catch((error: unknown) => {
303
+ console.error("Failed to load TUI plugins", error)
304
+ })
305
+ .finally(() => {
306
+ setReady(true)
307
+ })
308
+
309
+ // Let selection copy/dismiss win ahead of normal bindings when the feature flag is on.
310
+ const offSelectionKeys = keymap.intercept(
311
+ "key",
312
+ ({ event }: { event: any }) => {
313
+ if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
314
+ Selection.handleSelectionKey(renderer, toast, event)
315
+ },
316
+ { priority: 1 },
317
+ )
318
+ onCleanup(offSelectionKeys)
319
+
320
+ // Wire up console copy-to-clipboard via opentui's onCopySelection callback
321
+ renderer.console.onCopySelection = async (text: string) => {
322
+ if (!text || text.length === 0) return
323
+
324
+ await Clipboard.copy(text)
325
+ .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
326
+ .catch(toast.error)
327
+
328
+ renderer.clearSelection()
329
+ }
330
+ const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
331
+ const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal(kv.get("paste_summary_enabled", true))
332
+
333
+ // Update terminal window title based on current route and session
334
+ createEffect(() => {
335
+ if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return
336
+
337
+ if (route.data.type === "home") {
338
+ renderer.setTerminalTitle("APX")
339
+ return
340
+ }
341
+
342
+ if (route.data.type === "session") {
343
+ const session = sync.session.get(route.data.sessionID)
344
+ if (!session || !session.title || session.title === "New session") {
345
+ renderer.setTerminalTitle("APX")
346
+ return
347
+ }
348
+
349
+ const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
350
+ renderer.setTerminalTitle(`APX | ${title}`)
351
+ return
352
+ }
353
+
354
+ if (route.data.type === "plugin") {
355
+ renderer.setTerminalTitle(`APX | ${route.data.id}`)
356
+ }
357
+ })
358
+
359
+ const args = useArgs()
360
+ onMount(() => {
361
+ batch(() => {
362
+ if (args.agent) local.agent.set(args.agent)
363
+ if (args.model) {
364
+ // APX uses simple model strings — just set it directly via local store
365
+ // The model is already configured in the SDK; just navigate if needed
366
+ }
367
+ if (args.sessionID && !args.fork) {
368
+ route.navigate({
369
+ type: "session",
370
+ sessionID: args.sessionID,
371
+ })
372
+ }
373
+ })
374
+ })
375
+
376
+ const connected = useConnected()
377
+ const appCommands = createMemo(() =>
378
+ [
379
+ {
380
+ name: "command.palette.show",
381
+ title: "Show command palette",
382
+ category: "System",
383
+ hidden: true,
384
+ run: () => {
385
+ command.show()
386
+ },
387
+ },
388
+ {
389
+ name: "session.list",
390
+ title: "Switch session",
391
+ category: "Session",
392
+ suggested: sync.session.list().length > 0,
393
+ slashName: "sessions",
394
+ slashAliases: ["resume", "continue"],
395
+ run: () => {
396
+ dialog.replace(() => <DialogSessionList />)
397
+ },
398
+ },
399
+ {
400
+ name: "session.new",
401
+ title: "New session",
402
+ suggested: route.data.type === "session",
403
+ category: "Session",
404
+ slashName: "new",
405
+ slashAliases: ["clear"],
406
+ run: () => {
407
+ route.navigate({
408
+ type: "home",
409
+ })
410
+ dialog.clear()
411
+ },
412
+ },
413
+ {
414
+ name: "model.list",
415
+ title: "Switch model",
416
+ suggested: true,
417
+ category: "Agent",
418
+ slashName: "models",
419
+ run: () => {
420
+ dialog.replace(() => <DialogModel />)
421
+ },
422
+ },
423
+ {
424
+ name: "model.cycle_recent",
425
+ title: "Model cycle",
426
+ category: "Agent",
427
+ hidden: true,
428
+ run: () => {
429
+ local.model.cycle(1)
430
+ },
431
+ },
432
+ {
433
+ name: "model.cycle_recent_reverse",
434
+ title: "Model cycle reverse",
435
+ category: "Agent",
436
+ hidden: true,
437
+ run: () => {
438
+ local.model.cycle(-1)
439
+ },
440
+ },
441
+ {
442
+ name: "model.cycle_favorite",
443
+ title: "Favorite cycle",
444
+ category: "Agent",
445
+ hidden: true,
446
+ run: () => {
447
+ local.model.cycleFavorite(1)
448
+ },
449
+ },
450
+ {
451
+ name: "model.cycle_favorite_reverse",
452
+ title: "Favorite cycle reverse",
453
+ category: "Agent",
454
+ hidden: true,
455
+ run: () => {
456
+ local.model.cycleFavorite(-1)
457
+ },
458
+ },
459
+ {
460
+ name: "agent.list",
461
+ title: "Switch agent",
462
+ category: "Agent",
463
+ slashName: "agents",
464
+ run: () => {
465
+ dialog.replace(() => <DialogAgent />)
466
+ },
467
+ },
468
+ {
469
+ name: "mcp.list",
470
+ title: "Toggle MCPs",
471
+ category: "Agent",
472
+ slashName: "mcps",
473
+ run: () => {
474
+ dialog.replace(() => <DialogMcp />)
475
+ },
476
+ },
477
+ {
478
+ name: "agent.cycle",
479
+ title: "Agent cycle",
480
+ category: "Agent",
481
+ hidden: true,
482
+ run: () => {
483
+ local.agent.move(1)
484
+ },
485
+ },
486
+ {
487
+ name: "variant.cycle",
488
+ title: "Variant cycle",
489
+ category: "Agent",
490
+ run: () => {
491
+ local.model.variant.cycle()
492
+ },
493
+ },
494
+ {
495
+ name: "variant.list",
496
+ title: "Switch model variant",
497
+ category: "Agent",
498
+ hidden: local.model.variant.list().length === 0,
499
+ slashName: "variants",
500
+ run: () => {
501
+ dialog.replace(() => <DialogVariant />)
502
+ },
503
+ },
504
+ {
505
+ name: "agent.cycle.reverse",
506
+ title: "Agent cycle reverse",
507
+ category: "Agent",
508
+ hidden: true,
509
+ run: () => {
510
+ local.agent.move(-1)
511
+ },
512
+ },
513
+ {
514
+ name: "opencode.status",
515
+ title: "View status",
516
+ slashName: "status",
517
+ run: () => {
518
+ dialog.replace(() => <DialogStatus />)
519
+ },
520
+ category: "System",
521
+ },
522
+ {
523
+ name: "theme.switch",
524
+ title: "Switch theme",
525
+ slashName: "themes",
526
+ run: () => {
527
+ dialog.replace(() => <DialogThemeList />)
528
+ },
529
+ category: "System",
530
+ },
531
+ {
532
+ name: "theme.switch_mode",
533
+ title: mode() === "dark" ? "Switch to light mode" : "Switch to dark mode",
534
+ run: () => {
535
+ setMode(mode() === "dark" ? "light" : "dark")
536
+ dialog.clear()
537
+ },
538
+ category: "System",
539
+ },
540
+ {
541
+ name: "theme.mode.lock",
542
+ title: locked() ? "Unlock theme mode" : "Lock theme mode",
543
+ run: () => {
544
+ if (locked()) unlock()
545
+ else lock()
546
+ dialog.clear()
547
+ },
548
+ category: "System",
549
+ },
550
+ {
551
+ name: "help.show",
552
+ title: "Help",
553
+ slashName: "help",
554
+ run: () => {
555
+ dialog.replace(() => <DialogHelp />)
556
+ },
557
+ category: "System",
558
+ },
559
+ {
560
+ name: "app.exit",
561
+ title: "Exit the app",
562
+ slashName: "exit",
563
+ slashAliases: ["quit", "q"],
564
+ run: () => exit(),
565
+ category: "System",
566
+ },
567
+ {
568
+ name: "app.debug",
569
+ title: "Toggle debug panel",
570
+ category: "System",
571
+ run: () => {
572
+ renderer.toggleDebugOverlay()
573
+ dialog.clear()
574
+ },
575
+ },
576
+ {
577
+ name: "app.console",
578
+ title: "Toggle console",
579
+ category: "System",
580
+ run: () => {
581
+ renderer.console.toggle()
582
+ dialog.clear()
583
+ },
584
+ },
585
+ {
586
+ name: "app.heap_snapshot",
587
+ title: "Write heap snapshot",
588
+ category: "System",
589
+ run: async () => {
590
+ const files = await props.onSnapshot?.()
591
+ toast.show({
592
+ variant: "info",
593
+ message: `Heap snapshot written to ${files?.join(", ")}`,
594
+ duration: 5000,
595
+ })
596
+ dialog.clear()
597
+ },
598
+ },
599
+ {
600
+ name: "terminal.suspend",
601
+ title: "Suspend terminal",
602
+ category: "System",
603
+ hidden: true,
604
+ enabled: process.platform !== "win32",
605
+ run: () => {
606
+ process.once("SIGCONT", () => {
607
+ renderer.resume()
608
+ })
609
+
610
+ renderer.suspend()
611
+ process.kill(0, "SIGTSTP")
612
+ },
613
+ },
614
+ {
615
+ name: "terminal.title.toggle",
616
+ title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title",
617
+ category: "System",
618
+ run: () => {
619
+ setTerminalTitleEnabled((prev) => {
620
+ const next = !prev
621
+ kv.set("terminal_title_enabled", next)
622
+ if (!next) renderer.setTerminalTitle("")
623
+ return next
624
+ })
625
+ dialog.clear()
626
+ },
627
+ },
628
+ {
629
+ name: "app.toggle.animations",
630
+ title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations",
631
+ category: "System",
632
+ run: () => {
633
+ kv.set("animations_enabled", !kv.get("animations_enabled", true))
634
+ dialog.clear()
635
+ },
636
+ },
637
+ {
638
+ name: "app.toggle.file_context",
639
+ title: kv.get("file_context_enabled", true) ? "Disable file context" : "Enable file context",
640
+ category: "System",
641
+ run: () => {
642
+ kv.set("file_context_enabled", !kv.get("file_context_enabled", true))
643
+ dialog.clear()
644
+ },
645
+ },
646
+ {
647
+ name: "app.toggle.diffwrap",
648
+ title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
649
+ category: "System",
650
+ run: () => {
651
+ const current = kv.get("diff_wrap_mode", "word")
652
+ kv.set("diff_wrap_mode", current === "word" ? "none" : "word")
653
+ dialog.clear()
654
+ },
655
+ },
656
+ {
657
+ name: "app.toggle.paste_summary",
658
+ title: pasteSummaryEnabled() ? "Disable paste summary" : "Enable paste summary",
659
+ category: "System",
660
+ run: () => {
661
+ setPasteSummaryEnabled((prev) => {
662
+ const next = !prev
663
+ kv.set("paste_summary_enabled", next)
664
+ return next
665
+ })
666
+ dialog.clear()
667
+ },
668
+ },
669
+ {
670
+ name: "app.toggle.session_directory_filter",
671
+ title: kv.get("session_directory_filter_enabled", true)
672
+ ? "Disable session directory filtering"
673
+ : "Enable session directory filtering",
674
+ category: "System",
675
+ run: async () => {
676
+ kv.set("session_directory_filter_enabled", !kv.get("session_directory_filter_enabled", true))
677
+ await sync.session.refresh()
678
+ dialog.clear()
679
+ },
680
+ },
681
+ ].map((command) => ({
682
+ namespace: "palette",
683
+ ...command,
684
+ })),
685
+ )
686
+
687
+ useBindings(() => ({
688
+ commands: appCommands(),
689
+ }))
690
+
691
+ useBindings(() => ({
692
+ enabled: command.matcher,
693
+ bindings: tuiConfig.keybinds.gather(
694
+ "app",
695
+ Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
696
+ ? appBindingCommands
697
+ : appBindingCommands.filter(
698
+ (c) => !c.startsWith("session.cycle_recent") && !c.startsWith("session.quick_switch"),
699
+ ),
700
+ ),
701
+ }))
702
+
703
+ useBindings(() => ({
704
+ enabled: () => {
705
+ const ok = command.matcher.get()
706
+ if (!ok) return false
707
+ const current = promptRef.current
708
+ if (!current?.focused) return true
709
+ return current.current.input === ""
710
+ },
711
+ bindings: tuiConfig.keybinds.gather("app_exit", ["app.exit"]),
712
+ }))
713
+
714
+ event.on(TuiEvent.CommandExecute.type as any, (evt: any) => {
715
+ command.run(evt.properties.command)
716
+ })
717
+
718
+ event.on(TuiEvent.ToastShow.type as any, (evt: any) => {
719
+ toast.show({
720
+ title: evt.properties.title,
721
+ message: evt.properties.message,
722
+ variant: evt.properties.variant,
723
+ duration: evt.properties.duration,
724
+ })
725
+ })
726
+
727
+ event.on(TuiEvent.SessionSelect.type as any, (evt: any) => {
728
+ route.navigate({
729
+ type: "session",
730
+ sessionID: evt.properties.sessionID,
731
+ })
732
+ })
733
+
734
+ const plugin = createMemo(() => {
735
+ if (!ready()) return
736
+ if (route.data.type !== "plugin") return
737
+ const render = routeView(route.data.id)
738
+ if (!render) return <PluginRouteMissing id={route.data.id} onHome={() => route.navigate({ type: "home" })} />
739
+ return render({ params: route.data.data })
740
+ })
741
+
742
+ return (
743
+ <box
744
+ width={dimensions().width}
745
+ height={dimensions().height}
746
+ flexDirection="column"
747
+ backgroundColor={theme.background}
748
+ onMouseDown={(evt: any) => {
749
+ if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
750
+ if (evt.button !== MouseButton.RIGHT) return
751
+
752
+ if (!Selection.copy(renderer, toast)) return
753
+ evt.preventDefault()
754
+ evt.stopPropagation()
755
+ }}
756
+ onMouseUp={
757
+ Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? undefined : () => Selection.copy(renderer, toast)
758
+ }
759
+ >
760
+ <Show when={Flag.OPENCODE_SHOW_TTFD}>
761
+ <TimeToFirstDraw />
762
+ </Show>
763
+ <Show when={ready()}>
764
+ <box flexGrow={1} minHeight={0} flexDirection="column">
765
+ <Switch>
766
+ <Match when={route.data.type === "home"}>
767
+ <Home />
768
+ </Match>
769
+ <Match when={route.data.type === "session"}>
770
+ <Session />
771
+ </Match>
772
+ </Switch>
773
+ {plugin()}
774
+ </box>
775
+ <box flexShrink={0}>
776
+ <TuiPluginRuntime.Slot name="app_bottom" />
777
+ </box>
778
+ <TuiPluginRuntime.Slot name="app" />
779
+ </Show>
780
+ <StartupLoading ready={ready} />
781
+ </box>
782
+ )
783
+ }