@agentprojectcontext/apx 1.15.5 → 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 (222) 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/wakeup.js +14 -19
  30. package/src/daemon/whisper-server.py +57 -6
  31. package/src/overlay/index.html +44 -0
  32. package/src/overlay/main.js +480 -0
  33. package/src/overlay/package.json +3 -0
  34. package/src/overlay/preload.js +34 -0
  35. package/src/overlay/renderer.js +371 -0
  36. package/src/overlay/style.css +250 -0
  37. package/src/tui/_shims/cli-error.ts +6 -0
  38. package/src/tui/_shims/cli-logo.ts +18 -0
  39. package/src/tui/_shims/cli-ui.ts +1 -0
  40. package/src/tui/_shims/config-console-state.ts +7 -0
  41. package/src/tui/_shims/core-any.ts +30 -0
  42. package/src/tui/_shims/core-binary.ts +13 -0
  43. package/src/tui/_shims/core-flag.ts +3 -0
  44. package/src/tui/_shims/core-log.ts +14 -0
  45. package/src/tui/_shims/lsp-language.ts +1 -0
  46. package/src/tui/_shims/opencode-any.ts +135 -0
  47. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  48. package/src/tui/_shims/plugin-tui.ts +13 -0
  49. package/src/tui/_shims/provider-provider.ts +10 -0
  50. package/src/tui/_shims/session-retry.ts +1 -0
  51. package/src/tui/_shims/session-schema.ts +15 -0
  52. package/src/tui/_shims/session-session.ts +3 -0
  53. package/src/tui/_shims/snapshot.ts +4 -0
  54. package/src/tui/_shims/tool-any.ts +18 -0
  55. package/src/tui/_shims/util-error.ts +7 -0
  56. package/src/tui/_shims/util-filesystem.ts +79 -0
  57. package/src/tui/_shims/util-format.ts +7 -0
  58. package/src/tui/_shims/util-iife.ts +3 -0
  59. package/src/tui/_shims/util-locale.ts +10 -0
  60. package/src/tui/_shims/util-process.ts +38 -0
  61. package/src/tui/app.tsx +783 -0
  62. package/src/tui/asset/charge.wav +0 -0
  63. package/src/tui/asset/pulse-a.wav +0 -0
  64. package/src/tui/asset/pulse-b.wav +0 -0
  65. package/src/tui/asset/pulse-c.wav +0 -0
  66. package/src/tui/attach.ts +100 -0
  67. package/src/tui/component/bg-pulse-render.ts +436 -0
  68. package/src/tui/component/bg-pulse.tsx +99 -0
  69. package/src/tui/component/border.tsx +21 -0
  70. package/src/tui/component/dialog-agent.tsx +31 -0
  71. package/src/tui/component/dialog-console-org.tsx +103 -0
  72. package/src/tui/component/dialog-mcp.tsx +85 -0
  73. package/src/tui/component/dialog-model.tsx +175 -0
  74. package/src/tui/component/dialog-provider.tsx +456 -0
  75. package/src/tui/component/dialog-retry-action.tsx +160 -0
  76. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  77. package/src/tui/component/dialog-session-list.tsx +323 -0
  78. package/src/tui/component/dialog-session-rename.tsx +31 -0
  79. package/src/tui/component/dialog-skill.tsx +36 -0
  80. package/src/tui/component/dialog-stash.tsx +87 -0
  81. package/src/tui/component/dialog-status.tsx +168 -0
  82. package/src/tui/component/dialog-tag.tsx +44 -0
  83. package/src/tui/component/dialog-theme-list.tsx +50 -0
  84. package/src/tui/component/dialog-variant.tsx +39 -0
  85. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  86. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  87. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  88. package/src/tui/component/error-component.tsx +92 -0
  89. package/src/tui/component/logo.tsx +896 -0
  90. package/src/tui/component/plugin-route-missing.tsx +14 -0
  91. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  92. package/src/tui/component/prompt/cwd.ts +0 -0
  93. package/src/tui/component/prompt/frecency.tsx +90 -0
  94. package/src/tui/component/prompt/history.tsx +108 -0
  95. package/src/tui/component/prompt/index.tsx +1809 -0
  96. package/src/tui/component/prompt/part.ts +16 -0
  97. package/src/tui/component/prompt/stash.tsx +101 -0
  98. package/src/tui/component/prompt/traits.ts +35 -0
  99. package/src/tui/component/spinner.tsx +24 -0
  100. package/src/tui/component/startup-loading.tsx +63 -0
  101. package/src/tui/component/todo-item.tsx +32 -0
  102. package/src/tui/component/use-connected.tsx +9 -0
  103. package/src/tui/component/workspace-label.tsx +19 -0
  104. package/src/tui/config/cwd.ts +5 -0
  105. package/src/tui/config/keybind.ts +432 -0
  106. package/src/tui/config/tui-migrate.ts +154 -0
  107. package/src/tui/config/tui-schema.ts +34 -0
  108. package/src/tui/config/tui.ts +46 -0
  109. package/src/tui/context/aggregate-failures.ts +34 -0
  110. package/src/tui/context/args.tsx +15 -0
  111. package/src/tui/context/command-palette.tsx +163 -0
  112. package/src/tui/context/directory.ts +15 -0
  113. package/src/tui/context/editor-zed.ts +283 -0
  114. package/src/tui/context/editor.ts +468 -0
  115. package/src/tui/context/event-apx.ts +22 -0
  116. package/src/tui/context/event.ts +6 -0
  117. package/src/tui/context/exit.tsx +60 -0
  118. package/src/tui/context/helper.tsx +25 -0
  119. package/src/tui/context/kv.tsx +81 -0
  120. package/src/tui/context/local.tsx +608 -0
  121. package/src/tui/context/path-format.tsx +39 -0
  122. package/src/tui/context/project-apx.tsx +48 -0
  123. package/src/tui/context/project.tsx +7 -0
  124. package/src/tui/context/prompt.tsx +18 -0
  125. package/src/tui/context/route.tsx +52 -0
  126. package/src/tui/context/sdk-apx.tsx +185 -0
  127. package/src/tui/context/sdk.tsx +6 -0
  128. package/src/tui/context/sync-apx.tsx +178 -0
  129. package/src/tui/context/sync-v2.tsx +16 -0
  130. package/src/tui/context/sync.tsx +118 -0
  131. package/src/tui/context/theme/aura.json +69 -0
  132. package/src/tui/context/theme/ayu.json +80 -0
  133. package/src/tui/context/theme/carbonfox.json +248 -0
  134. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  135. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  136. package/src/tui/context/theme/catppuccin.json +112 -0
  137. package/src/tui/context/theme/cobalt2.json +225 -0
  138. package/src/tui/context/theme/cursor.json +249 -0
  139. package/src/tui/context/theme/dracula.json +219 -0
  140. package/src/tui/context/theme/everforest.json +241 -0
  141. package/src/tui/context/theme/flexoki.json +237 -0
  142. package/src/tui/context/theme/github.json +233 -0
  143. package/src/tui/context/theme/gruvbox.json +242 -0
  144. package/src/tui/context/theme/kanagawa.json +77 -0
  145. package/src/tui/context/theme/lucent-orng.json +234 -0
  146. package/src/tui/context/theme/material.json +235 -0
  147. package/src/tui/context/theme/matrix.json +77 -0
  148. package/src/tui/context/theme/mercury.json +252 -0
  149. package/src/tui/context/theme/monokai.json +221 -0
  150. package/src/tui/context/theme/nightowl.json +221 -0
  151. package/src/tui/context/theme/nord.json +223 -0
  152. package/src/tui/context/theme/one-dark.json +84 -0
  153. package/src/tui/context/theme/opencode.json +245 -0
  154. package/src/tui/context/theme/orng.json +249 -0
  155. package/src/tui/context/theme/osaka-jade.json +93 -0
  156. package/src/tui/context/theme/palenight.json +222 -0
  157. package/src/tui/context/theme/rosepine.json +234 -0
  158. package/src/tui/context/theme/solarized.json +223 -0
  159. package/src/tui/context/theme/synthwave84.json +226 -0
  160. package/src/tui/context/theme/tokyonight.json +243 -0
  161. package/src/tui/context/theme/vercel.json +245 -0
  162. package/src/tui/context/theme/vesper.json +218 -0
  163. package/src/tui/context/theme/zenburn.json +223 -0
  164. package/src/tui/context/theme.tsx +1247 -0
  165. package/src/tui/context/tui-config.tsx +9 -0
  166. package/src/tui/event.ts +16 -0
  167. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  168. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  169. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  170. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  171. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  172. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  173. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  174. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  175. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  176. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  177. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  178. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  179. package/src/tui/keymap.tsx +166 -0
  180. package/src/tui/layer.ts +6 -0
  181. package/src/tui/plugin/api.tsx +381 -0
  182. package/src/tui/plugin/command-shim.ts +109 -0
  183. package/src/tui/plugin/internal.ts +33 -0
  184. package/src/tui/plugin/runtime.ts +1069 -0
  185. package/src/tui/plugin/slots.tsx +60 -0
  186. package/src/tui/routes/home.tsx +96 -0
  187. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  188. package/src/tui/routes/session/dialog-message.tsx +108 -0
  189. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  190. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  191. package/src/tui/routes/session/footer.tsx +91 -0
  192. package/src/tui/routes/session/index.tsx +188 -0
  193. package/src/tui/routes/session/permission.tsx +722 -0
  194. package/src/tui/routes/session/question.tsx +490 -0
  195. package/src/tui/routes/session/sidebar.tsx +102 -0
  196. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  197. package/src/tui/run.ts +84 -0
  198. package/src/tui/thread.ts +261 -0
  199. package/src/tui/tsconfig.json +40 -0
  200. package/src/tui/ui/dialog-alert.tsx +66 -0
  201. package/src/tui/ui/dialog-confirm.tsx +108 -0
  202. package/src/tui/ui/dialog-export-options.tsx +217 -0
  203. package/src/tui/ui/dialog-help.tsx +40 -0
  204. package/src/tui/ui/dialog-prompt.tsx +101 -0
  205. package/src/tui/ui/dialog-select.tsx +553 -0
  206. package/src/tui/ui/dialog.tsx +211 -0
  207. package/src/tui/ui/link.tsx +34 -0
  208. package/src/tui/ui/spinner.ts +368 -0
  209. package/src/tui/ui/toast.tsx +111 -0
  210. package/src/tui/util/clipboard.ts +217 -0
  211. package/src/tui/util/editor.ts +37 -0
  212. package/src/tui/util/model.ts +23 -0
  213. package/src/tui/util/provider-origin.ts +7 -0
  214. package/src/tui/util/revert-diff.ts +18 -0
  215. package/src/tui/util/scroll.ts +25 -0
  216. package/src/tui/util/selection.ts +65 -0
  217. package/src/tui/util/signal.ts +41 -0
  218. package/src/tui/util/sound.ts +156 -0
  219. package/src/tui/util/transcript.ts +112 -0
  220. package/src/tui/validate-session.ts +29 -0
  221. package/src/tui/win32.ts +130 -0
  222. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,166 @@
1
+ import { type CliRenderer } from "@opentui/core"
2
+ import * as addons from "@opentui/keymap/addons/opentui"
3
+ import { stringifyKeyStroke } from "@opentui/keymap"
4
+ import {
5
+ formatCommandBindings as formatCommandBindingsExtra,
6
+ formatKeySequence as formatKeySequenceExtra,
7
+ } from "@opentui/keymap/extras"
8
+ import {
9
+ KeymapProvider,
10
+ reactiveMatcherFromSignal,
11
+ useKeymap,
12
+ useKeymapSelector,
13
+ useBindings,
14
+ } from "@opentui/keymap/solid"
15
+ import type { Accessor } from "solid-js"
16
+ import type { TuiConfig } from "./config/tui"
17
+ import { useTuiConfig } from "./context/tui-config"
18
+ import { TuiKeybind } from "./config/keybind"
19
+
20
+ export const LEADER_TOKEN = "leader"
21
+
22
+ export const OpencodeKeymapProvider = KeymapProvider
23
+ export const useOpencodeKeymap = useKeymap
24
+
25
+ export { reactiveMatcherFromSignal, useBindings, useKeymapSelector }
26
+
27
+ export type OpenTuiKeymap = ReturnType<typeof useKeymap>
28
+
29
+ const KEY_ALIASES = {
30
+ enter: "return",
31
+ esc: "escape",
32
+ } as const
33
+
34
+ function expandKeyAliases(input: string) {
35
+ const result = Object.entries(KEY_ALIASES).reduce(
36
+ (acc, [alias, key]) => acc.replace(new RegExp(`(^|[+,\\s>])${alias}(?=$|[+,\\s<])`, "gi"), `$1${key}`),
37
+ input,
38
+ )
39
+ if (result === input) return
40
+ return result
41
+ }
42
+
43
+ function registerKeyAliases(keymap: OpenTuiKeymap) {
44
+ return keymap.appendBindingExpander((ctx) => {
45
+ const key = expandKeyAliases(ctx.input)
46
+ if (!key) return
47
+ return [{ key, displays: ctx.displays }]
48
+ })
49
+ }
50
+
51
+ const inputCommands = [
52
+ "input.move.left",
53
+ "input.move.right",
54
+ "input.move.up",
55
+ "input.move.down",
56
+ "input.select.left",
57
+ "input.select.right",
58
+ "input.select.up",
59
+ "input.select.down",
60
+ "input.line.home",
61
+ "input.line.end",
62
+ "input.select.line.home",
63
+ "input.select.line.end",
64
+ "input.visual.line.home",
65
+ "input.visual.line.end",
66
+ "input.select.visual.line.home",
67
+ "input.select.visual.line.end",
68
+ "input.buffer.home",
69
+ "input.buffer.end",
70
+ "input.select.buffer.home",
71
+ "input.select.buffer.end",
72
+ "input.delete.line",
73
+ "input.delete.to.line.end",
74
+ "input.delete.to.line.start",
75
+ "input.backspace",
76
+ "input.delete",
77
+ "input.newline",
78
+ "input.undo",
79
+ "input.redo",
80
+ "input.word.forward",
81
+ "input.word.backward",
82
+ "input.select.word.forward",
83
+ "input.select.word.backward",
84
+ "input.delete.word.forward",
85
+ "input.delete.word.backward",
86
+ "input.select.all",
87
+ "input.submit",
88
+ ] as const
89
+
90
+ function leaderDisplay(config: TuiConfig.Resolved) {
91
+ const key = config.keybinds.get(LEADER_TOKEN)?.[0]?.key
92
+ if (!key) return TuiKeybind.LeaderDefault
93
+ return typeof key === "string" ? key : stringifyKeyStroke(key)
94
+ }
95
+
96
+ function formatOptions(config: TuiConfig.Resolved) {
97
+ return {
98
+ tokenDisplay: {
99
+ [LEADER_TOKEN]: leaderDisplay(config),
100
+ },
101
+ keyNameAliases: {
102
+ pageup: "pgup",
103
+ pagedown: "pgdn",
104
+ delete: "del",
105
+ },
106
+ modifierAliases: {
107
+ meta: "alt",
108
+ },
109
+ } as const
110
+ }
111
+
112
+ export function formatKeySequence(parts: Parameters<typeof formatKeySequenceExtra>[0], config: TuiConfig.Resolved) {
113
+ return formatKeySequenceExtra(parts, formatOptions(config))
114
+ }
115
+
116
+ export function formatKeyBindings(
117
+ bindings: Parameters<typeof formatCommandBindingsExtra>[0],
118
+ config: TuiConfig.Resolved,
119
+ ) {
120
+ return formatCommandBindingsExtra(bindings, formatOptions(config))
121
+ }
122
+
123
+ export function registerOpencodeKeymap(
124
+ keymap: OpenTuiKeymap,
125
+ renderer: CliRenderer,
126
+ config: Pick<TuiConfig.Resolved, "keybinds" | "leader_timeout">,
127
+ ) {
128
+ const offCommaBindings = addons.registerCommaBindings(keymap)
129
+ const offAliasExpander = registerKeyAliases(keymap)
130
+ const offBaseLayout = addons.registerBaseLayoutFallback(keymap)
131
+ const offLeader = addons.registerTimedLeader(keymap, {
132
+ trigger: config.keybinds.get(LEADER_TOKEN),
133
+ name: LEADER_TOKEN,
134
+ timeoutMs: config.leader_timeout,
135
+ })
136
+ const offEscape = addons.registerEscapeClearsPendingSequence(keymap)
137
+ const offBackspace = addons.registerBackspacePopsPendingSequence(keymap)
138
+ const offInputBindings = addons.registerManagedTextareaLayer(keymap, renderer, {
139
+ enabled: () => renderer.currentFocusedEditor !== null,
140
+ bindings: config.keybinds.gather("input", inputCommands),
141
+ })
142
+
143
+ return () => {
144
+ offInputBindings()
145
+ offBackspace()
146
+ offEscape()
147
+ offLeader()
148
+ offAliasExpander()
149
+ offBaseLayout()
150
+ offCommaBindings()
151
+ }
152
+ }
153
+
154
+ export function useCommandShortcut(command: string): Accessor<string> {
155
+ const config = useTuiConfig()
156
+ return useKeymapSelector((keymap) =>
157
+ formatKeySequence(
158
+ keymap.getCommandBindings({ visibility: "registered", commands: [command] }).get(command)?.[0]?.sequence,
159
+ config,
160
+ ),
161
+ )
162
+ }
163
+
164
+ export function useLeaderActive(): Accessor<boolean> {
165
+ return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN)
166
+ }
@@ -0,0 +1,6 @@
1
+ import { Layer } from "effect"
2
+ import { TuiConfig } from "./config/tui"
3
+ import { Npm } from "@opencode-ai/core/npm"
4
+ import { Observability } from "@opencode-ai/core/effect/observability"
5
+
6
+ export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer))
@@ -0,0 +1,381 @@
1
+ import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
2
+ import type { useEvent } from "@tui/context/event"
3
+ import type { useRoute } from "@tui/context/route"
4
+ import type { useSDK } from "@tui/context/sdk"
5
+ import type { useSync } from "@tui/context/sync"
6
+ import type { useTheme } from "@tui/context/theme"
7
+ import { Dialog as DialogUI, type useDialog } from "@tui/ui/dialog"
8
+ import type { TuiConfig } from "@/cli/cmd/tui/config/tui"
9
+ import type { useOpencodeKeymap } from "../keymap"
10
+ import type { useKV } from "../context/kv"
11
+ import { DialogAlert } from "../ui/dialog-alert"
12
+ import { DialogConfirm } from "../ui/dialog-confirm"
13
+ import { DialogPrompt } from "../ui/dialog-prompt"
14
+ import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dialog-select"
15
+ import { Prompt } from "../component/prompt"
16
+ import { Slot as HostSlot } from "./slots"
17
+ import type { useToast } from "../ui/toast"
18
+ import { InstallationVersion } from "@opencode-ai/core/installation/version"
19
+ import * as Keymap from "../keymap"
20
+ import { createCommandShim } from "./command-shim"
21
+
22
+ type RouteEntry = {
23
+ key: symbol
24
+ render: TuiRouteDefinition["render"]
25
+ }
26
+
27
+ export type RouteMap = Map<string, RouteEntry[]>
28
+
29
+ type Input = {
30
+ tuiConfig: TuiConfig.Resolved
31
+ dialog: ReturnType<typeof useDialog>
32
+ keymap: ReturnType<typeof useOpencodeKeymap>
33
+ kv: ReturnType<typeof useKV>
34
+ route: ReturnType<typeof useRoute>
35
+ routes: RouteMap
36
+ bump: () => void
37
+ event: ReturnType<typeof useEvent>
38
+ sdk: ReturnType<typeof useSDK>
39
+ sync: ReturnType<typeof useSync>
40
+ theme: ReturnType<typeof useTheme>
41
+ toast: ReturnType<typeof useToast>
42
+ renderer: TuiPluginApi["renderer"]
43
+ }
44
+
45
+ function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
46
+ const key = Symbol()
47
+ for (const item of list) {
48
+ const prev = routes.get(item.name) ?? []
49
+ prev.push({ key, render: item.render })
50
+ routes.set(item.name, prev)
51
+ }
52
+ bump()
53
+
54
+ return () => {
55
+ for (const item of list) {
56
+ const prev = routes.get(item.name)
57
+ if (!prev) continue
58
+ const next = prev.filter((x) => x.key !== key)
59
+ if (!next.length) {
60
+ routes.delete(item.name)
61
+ continue
62
+ }
63
+ routes.set(item.name, next)
64
+ }
65
+ bump()
66
+ }
67
+ }
68
+
69
+ function routeNavigate(route: ReturnType<typeof useRoute>, name: string, params?: Record<string, unknown>) {
70
+ if (name === "home") {
71
+ route.navigate({ type: "home" })
72
+ return
73
+ }
74
+
75
+ if (name === "session") {
76
+ const sessionID = params?.sessionID
77
+ if (typeof sessionID !== "string") return
78
+ route.navigate({ type: "session", sessionID })
79
+ return
80
+ }
81
+
82
+ route.navigate({ type: "plugin", id: name, data: params })
83
+ }
84
+
85
+ function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]["current"] {
86
+ if (route.data.type === "home") return { name: "home" }
87
+ if (route.data.type === "session") {
88
+ return {
89
+ name: "session",
90
+ params: {
91
+ sessionID: route.data.sessionID,
92
+ prompt: route.data.prompt,
93
+ },
94
+ }
95
+ }
96
+
97
+ return {
98
+ name: route.data.id,
99
+ params: route.data.data,
100
+ }
101
+ }
102
+
103
+ function mapOption<Value>(item: TuiDialogSelectOption<Value>): SelectOption<Value> {
104
+ return {
105
+ ...item,
106
+ onSelect: () => item.onSelect?.(),
107
+ }
108
+ }
109
+
110
+ function pickOption<Value>(item: SelectOption<Value>): TuiDialogSelectOption<Value> {
111
+ return {
112
+ title: item.title,
113
+ value: item.value,
114
+ description: item.description,
115
+ footer: item.footer,
116
+ category: item.category,
117
+ disabled: item.disabled,
118
+ }
119
+ }
120
+
121
+ function mapOptionCb<Value>(cb?: (item: TuiDialogSelectOption<Value>) => void) {
122
+ if (!cb) return
123
+ return (item: SelectOption<Value>) => cb(pickOption(item))
124
+ }
125
+
126
+ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
127
+ return {
128
+ get ready() {
129
+ return sync.ready
130
+ },
131
+ get config() {
132
+ return sync.data.config
133
+ },
134
+ get provider() {
135
+ return sync.data.provider
136
+ },
137
+ get path() {
138
+ return sync.path
139
+ },
140
+ get vcs() {
141
+ if (!sync.data.vcs) return
142
+ return {
143
+ branch: sync.data.vcs.branch,
144
+ }
145
+ },
146
+ session: {
147
+ count() {
148
+ return sync.data.session.length
149
+ },
150
+ get(sessionID) {
151
+ return sync.session.get(sessionID)
152
+ },
153
+ diff(sessionID) {
154
+ return (sync.data.session_diff[sessionID] ?? []).flatMap((item) =>
155
+ item.file === undefined ? [] : [{ ...item, file: item.file }],
156
+ )
157
+ },
158
+ todo(sessionID) {
159
+ return sync.data.todo[sessionID] ?? []
160
+ },
161
+ messages(sessionID) {
162
+ return sync.data.message[sessionID] ?? []
163
+ },
164
+ status(sessionID) {
165
+ return sync.data.session_status[sessionID]
166
+ },
167
+ permission(sessionID) {
168
+ return sync.data.permission[sessionID] ?? []
169
+ },
170
+ question(sessionID) {
171
+ return sync.data.question[sessionID] ?? []
172
+ },
173
+ },
174
+ part(messageID) {
175
+ return sync.data.part[messageID] ?? []
176
+ },
177
+ lsp() {
178
+ return sync.data.lsp.map((item) => ({ id: item.id, root: item.root, status: item.status }))
179
+ },
180
+ mcp() {
181
+ return Object.entries(sync.data.mcp)
182
+ .sort(([a], [b]) => a.localeCompare(b))
183
+ .map(([name, item]) => ({
184
+ name,
185
+ status: item.status,
186
+ error: item.status === "failed" ? item.error : undefined,
187
+ }))
188
+ },
189
+ }
190
+ }
191
+
192
+ function appApi(): TuiPluginApi["app"] {
193
+ return {
194
+ get version() {
195
+ return InstallationVersion
196
+ },
197
+ }
198
+ }
199
+
200
+ export function createTuiApi(input: Input): TuiPluginApi {
201
+ const lifecycle: TuiPluginApi["lifecycle"] = {
202
+ signal: new AbortController().signal,
203
+ onDispose() {
204
+ return () => {}
205
+ },
206
+ }
207
+ return {
208
+ app: appApi(),
209
+ // Keep deprecated `api.command` working for v1 plugins; remove in v2.
210
+ command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
211
+ keys: {
212
+ formatSequence(parts) {
213
+ return Keymap.formatKeySequence(parts, input.tuiConfig)
214
+ },
215
+ formatBindings(bindings) {
216
+ return Keymap.formatKeyBindings(bindings, input.tuiConfig)
217
+ },
218
+ },
219
+ keymap: input.keymap,
220
+ route: {
221
+ register(list) {
222
+ return routeRegister(input.routes, list, input.bump)
223
+ },
224
+ navigate(name, params) {
225
+ routeNavigate(input.route, name, params)
226
+ },
227
+ get current() {
228
+ return routeCurrent(input.route)
229
+ },
230
+ },
231
+ ui: {
232
+ Dialog(props) {
233
+ return (
234
+ <DialogUI size={props.size} onClose={props.onClose}>
235
+ {props.children}
236
+ </DialogUI>
237
+ )
238
+ },
239
+ DialogAlert(props) {
240
+ return <DialogAlert {...props} />
241
+ },
242
+ DialogConfirm(props) {
243
+ return <DialogConfirm {...props} />
244
+ },
245
+ DialogPrompt(props) {
246
+ return <DialogPrompt {...props} description={props.description} />
247
+ },
248
+ DialogSelect(props) {
249
+ return (
250
+ <DialogSelect
251
+ title={props.title}
252
+ placeholder={props.placeholder}
253
+ options={props.options.map(mapOption)}
254
+ flat={props.flat}
255
+ onMove={mapOptionCb(props.onMove)}
256
+ onFilter={props.onFilter}
257
+ onSelect={mapOptionCb(props.onSelect)}
258
+ skipFilter={props.skipFilter}
259
+ current={props.current}
260
+ />
261
+ )
262
+ },
263
+ Slot<Name extends string>(props: TuiSlotProps<Name>) {
264
+ return <HostSlot {...props} />
265
+ },
266
+ Prompt(props) {
267
+ return (
268
+ <Prompt
269
+ sessionID={props.sessionID}
270
+ workspaceID={props.workspaceID}
271
+ visible={props.visible}
272
+ disabled={props.disabled}
273
+ onSubmit={props.onSubmit}
274
+ ref={props.ref}
275
+ hint={props.hint}
276
+ right={props.right}
277
+ showPlaceholder={props.showPlaceholder}
278
+ placeholders={props.placeholders}
279
+ />
280
+ )
281
+ },
282
+ toast(inputToast) {
283
+ input.toast.show({
284
+ title: inputToast.title,
285
+ message: inputToast.message,
286
+ variant: inputToast.variant ?? "info",
287
+ duration: inputToast.duration,
288
+ })
289
+ },
290
+ dialog: {
291
+ replace(render, onClose) {
292
+ input.dialog.replace(render, onClose)
293
+ },
294
+ clear() {
295
+ input.dialog.clear()
296
+ },
297
+ setSize(size) {
298
+ input.dialog.setSize(size)
299
+ },
300
+ get size() {
301
+ return input.dialog.size
302
+ },
303
+ get depth() {
304
+ return input.dialog.stack.length
305
+ },
306
+ get open() {
307
+ return input.dialog.stack.length > 0
308
+ },
309
+ },
310
+ },
311
+ get tuiConfig() {
312
+ return input.tuiConfig
313
+ },
314
+ kv: {
315
+ get(key, fallback) {
316
+ return input.kv.get(key, fallback)
317
+ },
318
+ set(key, value) {
319
+ input.kv.set(key, value)
320
+ },
321
+ get ready() {
322
+ return input.kv.ready
323
+ },
324
+ },
325
+ state: stateApi(input.sync),
326
+ get client() {
327
+ return input.sdk.client
328
+ },
329
+ event: input.event,
330
+ renderer: input.renderer,
331
+ slots: {
332
+ register() {
333
+ throw new Error("slots.register is only available in plugin context")
334
+ },
335
+ },
336
+ plugins: {
337
+ list() {
338
+ return []
339
+ },
340
+ async activate() {
341
+ return false
342
+ },
343
+ async deactivate() {
344
+ return false
345
+ },
346
+ async add() {
347
+ return false
348
+ },
349
+ async install() {
350
+ return {
351
+ ok: false,
352
+ message: "plugins.install is only available in plugin context",
353
+ }
354
+ },
355
+ },
356
+ lifecycle,
357
+ theme: {
358
+ get current() {
359
+ return input.theme.theme
360
+ },
361
+ get selected() {
362
+ return input.theme.selected
363
+ },
364
+ has(name) {
365
+ return input.theme.has(name)
366
+ },
367
+ set(name) {
368
+ return input.theme.set(name)
369
+ },
370
+ async install(_jsonPath) {
371
+ throw new Error("theme.install is only available in plugin context")
372
+ },
373
+ mode() {
374
+ return input.theme.mode()
375
+ },
376
+ get ready() {
377
+ return input.theme.ready
378
+ },
379
+ },
380
+ }
381
+ }
@@ -0,0 +1,109 @@
1
+ // Legacy `api.command` bridge for v1 plugins; remove in v2.
2
+ import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui"
3
+ import { TuiKeybind } from "../config/keybind"
4
+ import type { DialogContext } from "../ui/dialog"
5
+
6
+ const COMMAND_PALETTE_SHOW = "command.palette.show"
7
+ const warned = new Set<string>()
8
+
9
+ type Warn = (api: string, replacement: string) => void
10
+ type LegacyDialog = TuiPluginApi["ui"]["dialog"]
11
+ type CommandShimDialog = DialogContext | LegacyDialog
12
+ type LegacyKeybinds = TuiPluginApi["tuiConfig"]["keybinds"]
13
+
14
+ function warnCommandShim(api: string, replacement: string) {
15
+ // Warn v1 plugins about deprecated `api.command`; remove this shim path in v2.
16
+ console.warn("[tui.plugin] deprecated TUI plugin API", { api, replacement })
17
+ }
18
+
19
+ function createCommandShimDialog(dialog: CommandShimDialog): LegacyDialog {
20
+ if (!("stack" in dialog)) return dialog
21
+ return {
22
+ replace(render, onClose) {
23
+ dialog.replace(render, onClose)
24
+ },
25
+ clear() {
26
+ dialog.clear()
27
+ },
28
+ setSize(size) {
29
+ dialog.setSize(size)
30
+ },
31
+ get size() {
32
+ return dialog.size
33
+ },
34
+ get depth() {
35
+ return dialog.stack.length
36
+ },
37
+ get open() {
38
+ return dialog.stack.length > 0
39
+ },
40
+ }
41
+ }
42
+
43
+ function warnOnce(api: string, replacement: string, warn: Warn) {
44
+ if (warned.has(api)) return
45
+ warned.add(api)
46
+ warn(api, replacement)
47
+ }
48
+
49
+ function toCommand(item: TuiCommand, dialog: LegacyDialog) {
50
+ return {
51
+ namespace: "palette",
52
+ name: item.value,
53
+ title: item.title,
54
+ desc: item.description,
55
+ category: item.category,
56
+ suggested: item.suggested,
57
+ hidden: item.hidden,
58
+ enabled: item.enabled,
59
+ slashName: item.slash?.name,
60
+ slashAliases: item.slash?.aliases,
61
+ run() {
62
+ return item.onSelect?.(dialog)
63
+ },
64
+ }
65
+ }
66
+
67
+ function toBindings(commands: TuiCommand[], keybinds: LegacyKeybinds) {
68
+ return commands.flatMap((item) =>
69
+ item.keybind
70
+ ? keybinds.has(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
71
+ ? keybinds
72
+ .get(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
73
+ .map((binding) => ({ ...binding, cmd: item.value, desc: binding.desc ?? item.title }))
74
+ : [
75
+ {
76
+ key: item.keybind,
77
+ cmd: item.value,
78
+ desc: item.title,
79
+ },
80
+ ]
81
+ : [],
82
+ )
83
+ }
84
+
85
+ export function createCommandShim(
86
+ keymap: TuiPluginApi["keymap"],
87
+ dialog: CommandShimDialog,
88
+ keybinds: LegacyKeybinds,
89
+ ): TuiPluginApi["command"] {
90
+ const shimDialog = createCommandShimDialog(dialog)
91
+ return {
92
+ register(cb) {
93
+ warnOnce("api.command.register", "api.keymap.registerLayer({ commands, bindings })", warnCommandShim)
94
+ const commands = cb()
95
+ return keymap.registerLayer({
96
+ commands: commands.map((item) => toCommand(item, shimDialog)),
97
+ bindings: toBindings(commands, keybinds),
98
+ })
99
+ },
100
+ trigger(value) {
101
+ warnOnce("api.command.trigger", "api.keymap.dispatchCommand(name)", warnCommandShim)
102
+ keymap.dispatchCommand(value)
103
+ },
104
+ show() {
105
+ warnOnce("api.command.show", `api.keymap.dispatchCommand("${COMMAND_PALETTE_SHOW}")`, warnCommandShim)
106
+ keymap.dispatchCommand(COMMAND_PALETTE_SHOW)
107
+ },
108
+ }
109
+ }
@@ -0,0 +1,33 @@
1
+ import HomeFooter from "../feature-plugins/home/footer"
2
+ import HomeTips from "../feature-plugins/home/tips"
3
+ import SidebarContext from "../feature-plugins/sidebar/context"
4
+ import SidebarMcp from "../feature-plugins/sidebar/mcp"
5
+ import SidebarLsp from "../feature-plugins/sidebar/lsp"
6
+ import SidebarTodo from "../feature-plugins/sidebar/todo"
7
+ import SidebarFiles from "../feature-plugins/sidebar/files"
8
+ import SidebarFooter from "../feature-plugins/sidebar/footer"
9
+ import PluginManager from "../feature-plugins/system/plugins"
10
+ import SessionV2Debug from "../feature-plugins/system/session-v2"
11
+ import WhichKey from "../feature-plugins/system/which-key"
12
+ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
13
+ import { Flag } from "@opencode-ai/core/flag/flag"
14
+
15
+ export type InternalTuiPlugin = Omit<TuiPluginModule, "id"> & {
16
+ id: string
17
+ tui: TuiPlugin
18
+ enabled?: boolean
19
+ }
20
+
21
+ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
22
+ HomeFooter,
23
+ HomeTips,
24
+ SidebarContext,
25
+ SidebarMcp,
26
+ SidebarLsp,
27
+ SidebarTodo,
28
+ SidebarFiles,
29
+ SidebarFooter,
30
+ PluginManager,
31
+ WhichKey,
32
+ ...(Flag.OPENCODE_EXPERIMENTAL_EVENT_SYSTEM ? [SessionV2Debug] : []),
33
+ ]