@neurocode-ai/tui 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/bunfig.toml +4 -0
  2. package/package.json +72 -0
  3. package/src/app.tsx +1134 -0
  4. package/src/attention.ts +260 -0
  5. package/src/audio.d.ts +9 -0
  6. package/src/audio.ts +53 -0
  7. package/src/clipboard.ts +124 -0
  8. package/src/component/bg-pulse-render.ts +436 -0
  9. package/src/component/bg-pulse.tsx +99 -0
  10. package/src/component/command-palette.tsx +79 -0
  11. package/src/component/dialog-agent.tsx +31 -0
  12. package/src/component/dialog-console-org.tsx +135 -0
  13. package/src/component/dialog-debug.tsx +90 -0
  14. package/src/component/dialog-mcp.tsx +85 -0
  15. package/src/component/dialog-model.tsx +197 -0
  16. package/src/component/dialog-move-session.tsx +353 -0
  17. package/src/component/dialog-provider.tsx +469 -0
  18. package/src/component/dialog-retry-action.tsx +160 -0
  19. package/src/component/dialog-session-delete-failed.tsx +99 -0
  20. package/src/component/dialog-session-list.tsx +364 -0
  21. package/src/component/dialog-session-rename.tsx +31 -0
  22. package/src/component/dialog-skill.tsx +70 -0
  23. package/src/component/dialog-stash.tsx +87 -0
  24. package/src/component/dialog-status.tsx +168 -0
  25. package/src/component/dialog-tag.tsx +47 -0
  26. package/src/component/dialog-theme-list.tsx +50 -0
  27. package/src/component/dialog-variant.tsx +39 -0
  28. package/src/component/dialog-workspace-create.tsx +308 -0
  29. package/src/component/dialog-workspace-file-changes.tsx +144 -0
  30. package/src/component/dialog-workspace-list.tsx +112 -0
  31. package/src/component/dialog-workspace-unavailable.tsx +69 -0
  32. package/src/component/error-component.tsx +240 -0
  33. package/src/component/logo.tsx +61 -0
  34. package/src/component/plugin-route-missing.tsx +14 -0
  35. package/src/component/prompt/autocomplete.tsx +781 -0
  36. package/src/component/prompt/cwd.ts +0 -0
  37. package/src/component/prompt/frecency.tsx +1 -0
  38. package/src/component/prompt/history.tsx +1 -0
  39. package/src/component/prompt/index.tsx +1713 -0
  40. package/src/component/prompt/local-attachment.ts +48 -0
  41. package/src/component/prompt/move.tsx +205 -0
  42. package/src/component/prompt/stash.tsx +1 -0
  43. package/src/component/prompt/workspace.tsx +137 -0
  44. package/src/component/register-spinner.ts +6 -0
  45. package/src/component/spinner.tsx +26 -0
  46. package/src/component/startup-loading.tsx +63 -0
  47. package/src/component/todo-item.tsx +32 -0
  48. package/src/component/use-connected.tsx +12 -0
  49. package/src/component/workspace-label.tsx +19 -0
  50. package/src/config/index.tsx +129 -0
  51. package/src/config/keybind.ts +471 -0
  52. package/src/context/args.tsx +16 -0
  53. package/src/context/clipboard.tsx +18 -0
  54. package/src/context/data.tsx +569 -0
  55. package/src/context/directory.ts +17 -0
  56. package/src/context/editor.ts +408 -0
  57. package/src/context/epilogue.tsx +6 -0
  58. package/src/context/event.ts +36 -0
  59. package/src/context/exit.tsx +8 -0
  60. package/src/context/helper.tsx +26 -0
  61. package/src/context/kv.tsx +66 -0
  62. package/src/context/local.tsx +542 -0
  63. package/src/context/location.tsx +14 -0
  64. package/src/context/path-format.tsx +24 -0
  65. package/src/context/permission.tsx +26 -0
  66. package/src/context/project.tsx +115 -0
  67. package/src/context/prompt.tsx +18 -0
  68. package/src/context/route.tsx +60 -0
  69. package/src/context/runtime.tsx +62 -0
  70. package/src/context/sdk.tsx +151 -0
  71. package/src/context/sync.tsx +666 -0
  72. package/src/context/theme.tsx +332 -0
  73. package/src/context/thinking.ts +67 -0
  74. package/src/editor-zed.ts +286 -0
  75. package/src/editor.ts +101 -0
  76. package/src/feature-plugins/builtins.ts +36 -0
  77. package/src/feature-plugins/home/footer.tsx +100 -0
  78. package/src/feature-plugins/home/tips-view.tsx +287 -0
  79. package/src/feature-plugins/home/tips.tsx +59 -0
  80. package/src/feature-plugins/sidebar/context.tsx +65 -0
  81. package/src/feature-plugins/sidebar/files.tsx +70 -0
  82. package/src/feature-plugins/sidebar/footer.tsx +98 -0
  83. package/src/feature-plugins/sidebar/lsp.tsx +65 -0
  84. package/src/feature-plugins/sidebar/mcp.tsx +97 -0
  85. package/src/feature-plugins/sidebar/todo.tsx +49 -0
  86. package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
  87. package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
  88. package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
  89. package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
  90. package/src/feature-plugins/system/notifications.ts +94 -0
  91. package/src/feature-plugins/system/plugins.tsx +269 -0
  92. package/src/feature-plugins/system/which-key.tsx +608 -0
  93. package/src/index.tsx +1 -0
  94. package/src/keymap.tsx +290 -0
  95. package/src/logo.ts +11 -0
  96. package/src/parsers-config.ts +386 -0
  97. package/src/plugin/adapters.tsx +355 -0
  98. package/src/plugin/api.ts +52 -0
  99. package/src/plugin/command-shim.ts +109 -0
  100. package/src/plugin/runtime.tsx +81 -0
  101. package/src/plugin/slots.tsx +65 -0
  102. package/src/prompt/display.ts +48 -0
  103. package/src/prompt/frecency.tsx +80 -0
  104. package/src/prompt/history.tsx +111 -0
  105. package/src/prompt/part.ts +29 -0
  106. package/src/prompt/stash.tsx +89 -0
  107. package/src/prompt/traits.ts +29 -0
  108. package/src/routes/home/session-destination.tsx +41 -0
  109. package/src/routes/home.tsx +95 -0
  110. package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
  111. package/src/routes/session/dialog-message.tsx +109 -0
  112. package/src/routes/session/dialog-subagent.tsx +26 -0
  113. package/src/routes/session/dialog-timeline.tsx +47 -0
  114. package/src/routes/session/footer.tsx +91 -0
  115. package/src/routes/session/index.tsx +2710 -0
  116. package/src/routes/session/permission.tsx +718 -0
  117. package/src/routes/session/question.tsx +514 -0
  118. package/src/routes/session/sidebar.tsx +103 -0
  119. package/src/routes/session/subagent-footer.tsx +132 -0
  120. package/src/runtime.tsx +9 -0
  121. package/src/terminal-win32.ts +130 -0
  122. package/src/theme/assets/aura.json +69 -0
  123. package/src/theme/assets/ayu.json +80 -0
  124. package/src/theme/assets/carbonfox.json +248 -0
  125. package/src/theme/assets/catppuccin-frappe.json +230 -0
  126. package/src/theme/assets/catppuccin-macchiato.json +230 -0
  127. package/src/theme/assets/catppuccin.json +112 -0
  128. package/src/theme/assets/cobalt2.json +225 -0
  129. package/src/theme/assets/cursor.json +249 -0
  130. package/src/theme/assets/dracula.json +219 -0
  131. package/src/theme/assets/everforest.json +241 -0
  132. package/src/theme/assets/flexoki.json +237 -0
  133. package/src/theme/assets/github.json +233 -0
  134. package/src/theme/assets/gruvbox.json +242 -0
  135. package/src/theme/assets/kanagawa.json +77 -0
  136. package/src/theme/assets/lucent-orng.json +234 -0
  137. package/src/theme/assets/material.json +235 -0
  138. package/src/theme/assets/matrix.json +77 -0
  139. package/src/theme/assets/mercury.json +252 -0
  140. package/src/theme/assets/monokai.json +221 -0
  141. package/src/theme/assets/nightowl.json +221 -0
  142. package/src/theme/assets/nord.json +223 -0
  143. package/src/theme/assets/one-dark.json +84 -0
  144. package/src/theme/assets/opencode.json +245 -0
  145. package/src/theme/assets/orng.json +249 -0
  146. package/src/theme/assets/osaka-jade.json +93 -0
  147. package/src/theme/assets/palenight.json +222 -0
  148. package/src/theme/assets/rosepine.json +234 -0
  149. package/src/theme/assets/solarized.json +223 -0
  150. package/src/theme/assets/synthwave84.json +226 -0
  151. package/src/theme/assets/tokyonight.json +243 -0
  152. package/src/theme/assets/vercel.json +245 -0
  153. package/src/theme/assets/vesper.json +218 -0
  154. package/src/theme/assets/zenburn.json +223 -0
  155. package/src/theme/index.ts +1089 -0
  156. package/src/ui/border.ts +21 -0
  157. package/src/ui/dialog-alert.tsx +66 -0
  158. package/src/ui/dialog-confirm.tsx +108 -0
  159. package/src/ui/dialog-export-options.tsx +217 -0
  160. package/src/ui/dialog-help.tsx +40 -0
  161. package/src/ui/dialog-prompt.tsx +126 -0
  162. package/src/ui/dialog-select.tsx +790 -0
  163. package/src/ui/dialog.tsx +231 -0
  164. package/src/ui/link.tsx +34 -0
  165. package/src/ui/spinner.ts +368 -0
  166. package/src/ui/toast.tsx +102 -0
  167. package/src/util/collapse-tool-output.ts +19 -0
  168. package/src/util/error.ts +182 -0
  169. package/src/util/filetype.ts +130 -0
  170. package/src/util/format.ts +20 -0
  171. package/src/util/layout.ts +25 -0
  172. package/src/util/locale.ts +86 -0
  173. package/src/util/model.ts +28 -0
  174. package/src/util/path.ts +12 -0
  175. package/src/util/persistence.ts +33 -0
  176. package/src/util/presentation.ts +38 -0
  177. package/src/util/provider-origin.ts +7 -0
  178. package/src/util/record.ts +3 -0
  179. package/src/util/renderer.ts +7 -0
  180. package/src/util/revert-diff.ts +18 -0
  181. package/src/util/scroll.ts +27 -0
  182. package/src/util/selection.ts +79 -0
  183. package/src/util/session.ts +3 -0
  184. package/src/util/signal.ts +51 -0
  185. package/src/util/system.ts +20 -0
  186. package/src/util/tool-display.ts +13 -0
  187. package/src/util/transcript.ts +112 -0
  188. package/sst-env.d.ts +10 -0
  189. package/test/app-lifecycle.test.tsx +128 -0
  190. package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
  191. package/test/cli/cmd/tui/model-options.test.ts +32 -0
  192. package/test/cli/cmd/tui/notifications.test.ts +267 -0
  193. package/test/cli/cmd/tui/provider-options.test.ts +41 -0
  194. package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
  195. package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
  196. package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
  197. package/test/cli/cmd/tui/sync.test.tsx +65 -0
  198. package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
  199. package/test/cli/tui/data.test.tsx +486 -0
  200. package/test/cli/tui/dialog-prompt.test.tsx +147 -0
  201. package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
  202. package/test/cli/tui/diff-viewer.test.tsx +268 -0
  203. package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
  204. package/test/cli/tui/prompt-submit-race.test.ts +98 -0
  205. package/test/cli/tui/thinking.test.ts +36 -0
  206. package/test/cli/tui/use-event.test.tsx +148 -0
  207. package/test/clipboard.test.ts +19 -0
  208. package/test/component/dialog-session-list.test.ts +46 -0
  209. package/test/config.test.tsx +127 -0
  210. package/test/context/local.test.ts +22 -0
  211. package/test/editor.test.ts +32 -0
  212. package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
  213. package/test/fixture/fixture.ts +13 -0
  214. package/test/fixture/tui-environment.tsx +32 -0
  215. package/test/fixture/tui-plugin.ts +36 -0
  216. package/test/fixture/tui-runtime.ts +12 -0
  217. package/test/fixture/tui-sdk.ts +109 -0
  218. package/test/index.test.tsx +6 -0
  219. package/test/keymap.test.tsx +141 -0
  220. package/test/plugin/runtime.test.ts +50 -0
  221. package/test/plugin/slots.test.tsx +38 -0
  222. package/test/prompt/display.test.ts +33 -0
  223. package/test/prompt/history.test.ts +39 -0
  224. package/test/prompt/jsonl.test.ts +24 -0
  225. package/test/prompt/local-attachment.test.ts +43 -0
  226. package/test/prompt/part.test.ts +53 -0
  227. package/test/prompt/persistence.test.ts +23 -0
  228. package/test/prompt/traits.test.ts +25 -0
  229. package/test/runtime.test.tsx +37 -0
  230. package/test/theme.test.ts +81 -0
  231. package/test/util/error.test.ts +49 -0
  232. package/test/util/filetype.test.ts +16 -0
  233. package/test/util/format.test.ts +59 -0
  234. package/test/util/model.test.ts +9 -0
  235. package/test/util/presentation.test.ts +8 -0
  236. package/test/util/renderer.test.ts +30 -0
  237. package/test/util/revert-diff.test.ts +35 -0
  238. package/test/util/session.test.ts +10 -0
  239. package/test/util/tool-display.test.ts +40 -0
  240. package/test/util/transcript.test.ts +421 -0
  241. package/tsconfig.json +10 -0
@@ -0,0 +1,355 @@
1
+ import type { TuiDialogSelectOption, TuiPluginApi, TuiSlotProps } from "@neurocode-ai/plugin/tui"
2
+ import type { TuiConfig } from "../config"
3
+ import type { useEvent } from "../context/event"
4
+ import type { useRoute } from "../context/route"
5
+ import type { useSDK } from "../context/sdk"
6
+ import type { useSync } from "../context/sync"
7
+ import type { useTheme } from "../context/theme"
8
+ import { Dialog as DialogUI, type useDialog } from "../ui/dialog"
9
+ import type { useNeurocodeKeymap } 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 type { useToast } from "../ui/toast"
17
+ import * as Keymap from "../keymap"
18
+ import { createCommandShim } from "./command-shim"
19
+ import type { PluginRoutes } from "./api"
20
+ export type { RouteMap } from "./api"
21
+ export { createPluginRoutes, createTuiApi } from "./api"
22
+
23
+ type Input = {
24
+ version: string
25
+ tuiConfig: TuiConfig.Resolved
26
+ dialog: ReturnType<typeof useDialog>
27
+ keymap: ReturnType<typeof useNeurocodeKeymap>
28
+ kv: ReturnType<typeof useKV>
29
+ route: ReturnType<typeof useRoute>
30
+ routes: PluginRoutes
31
+ event: ReturnType<typeof useEvent>
32
+ sdk: ReturnType<typeof useSDK>
33
+ sync: ReturnType<typeof useSync>
34
+ theme: ReturnType<typeof useTheme>
35
+ toast: ReturnType<typeof useToast>
36
+ renderer: TuiPluginApi["renderer"]
37
+ attention: TuiPluginApi["attention"]
38
+ Slot: TuiPluginApi["ui"]["Slot"]
39
+ }
40
+
41
+ function routeNavigate(route: ReturnType<typeof useRoute>, name: string, params?: Record<string, unknown>) {
42
+ if (name === "home") {
43
+ route.navigate({ type: "home" })
44
+ return
45
+ }
46
+
47
+ if (name === "session") {
48
+ const sessionID = params?.sessionID
49
+ if (typeof sessionID !== "string") return
50
+ route.navigate({ type: "session", sessionID })
51
+ return
52
+ }
53
+
54
+ route.navigate({ type: "plugin", id: name, data: params })
55
+ }
56
+
57
+ function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]["current"] {
58
+ if (route.data.type === "home") return { name: "home" }
59
+ if (route.data.type === "session") {
60
+ return {
61
+ name: "session",
62
+ params: {
63
+ sessionID: route.data.sessionID,
64
+ prompt: route.data.prompt,
65
+ },
66
+ }
67
+ }
68
+
69
+ return {
70
+ name: route.data.id,
71
+ params: route.data.data,
72
+ }
73
+ }
74
+
75
+ function mapOption<Value>(item: TuiDialogSelectOption<Value>): SelectOption<Value> {
76
+ return {
77
+ ...item,
78
+ onSelect: () => item.onSelect?.(),
79
+ }
80
+ }
81
+
82
+ function pickOption<Value>(item: SelectOption<Value>): TuiDialogSelectOption<Value> {
83
+ return {
84
+ title: item.title,
85
+ value: item.value,
86
+ description: item.description,
87
+ footer: item.footer,
88
+ category: item.category,
89
+ disabled: item.disabled,
90
+ }
91
+ }
92
+
93
+ function mapOptionCb<Value>(cb?: (item: TuiDialogSelectOption<Value>) => void) {
94
+ if (!cb) return
95
+ return (item: SelectOption<Value>) => cb(pickOption(item))
96
+ }
97
+
98
+ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
99
+ return {
100
+ get ready() {
101
+ return sync.ready
102
+ },
103
+ get config() {
104
+ return sync.data.config
105
+ },
106
+ get provider() {
107
+ return sync.data.provider
108
+ },
109
+ get path() {
110
+ return sync.path
111
+ },
112
+ get vcs() {
113
+ if (!sync.data.vcs) return
114
+ return {
115
+ branch: sync.data.vcs.branch,
116
+ default_branch: sync.data.vcs.default_branch,
117
+ }
118
+ },
119
+ session: {
120
+ count() {
121
+ return sync.data.session.length
122
+ },
123
+ get(sessionID) {
124
+ return sync.session.get(sessionID)
125
+ },
126
+ diff(sessionID) {
127
+ return (sync.data.session_diff[sessionID] ?? []).flatMap((item) =>
128
+ item.file === undefined ? [] : [{ ...item, file: item.file }],
129
+ )
130
+ },
131
+ todo(sessionID) {
132
+ return sync.data.todo[sessionID] ?? []
133
+ },
134
+ messages(sessionID) {
135
+ return sync.data.message[sessionID] ?? []
136
+ },
137
+ status(sessionID) {
138
+ return sync.data.session_status[sessionID]
139
+ },
140
+ permission(sessionID) {
141
+ return sync.data.permission[sessionID] ?? []
142
+ },
143
+ question(sessionID) {
144
+ return sync.data.question[sessionID] ?? []
145
+ },
146
+ },
147
+ part(messageID) {
148
+ return sync.data.part[messageID] ?? []
149
+ },
150
+ lsp() {
151
+ return sync.data.lsp.map((item) => ({ id: item.id, root: item.root, status: item.status }))
152
+ },
153
+ mcp() {
154
+ return Object.entries(sync.data.mcp)
155
+ .sort(([a], [b]) => a.localeCompare(b))
156
+ .map(([name, item]) => ({
157
+ name,
158
+ status: item.status,
159
+ error: item.status === "failed" ? item.error : undefined,
160
+ }))
161
+ },
162
+ }
163
+ }
164
+
165
+ function appApi(version: string): TuiPluginApi["app"] {
166
+ return {
167
+ get version() {
168
+ return version
169
+ },
170
+ }
171
+ }
172
+
173
+ export function createTuiApiAdapters(input: Input): Omit<TuiPluginApi, "lifecycle"> {
174
+ return {
175
+ app: appApi(input.version),
176
+ attention: input.attention,
177
+ // Keep deprecated `api.command` working for v1 plugins; remove in v2.
178
+ command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
179
+ keys: {
180
+ formatSequence(parts) {
181
+ return Keymap.formatKeySequence(parts, input.tuiConfig)
182
+ },
183
+ formatBindings(bindings) {
184
+ return Keymap.formatKeyBindings(bindings, input.tuiConfig)
185
+ },
186
+ },
187
+ keymap: input.keymap,
188
+ mode: {
189
+ current() {
190
+ return Keymap.getNeurocodeModeStack(input.keymap).current()
191
+ },
192
+ push(mode) {
193
+ return Keymap.getNeurocodeModeStack(input.keymap).push(mode)
194
+ },
195
+ },
196
+ route: {
197
+ register(list) {
198
+ return input.routes.register(list)
199
+ },
200
+ navigate(name, params) {
201
+ routeNavigate(input.route, name, params)
202
+ },
203
+ get current() {
204
+ return routeCurrent(input.route)
205
+ },
206
+ },
207
+ ui: {
208
+ Dialog(props) {
209
+ return (
210
+ <DialogUI size={props.size} onClose={props.onClose}>
211
+ {props.children}
212
+ </DialogUI>
213
+ )
214
+ },
215
+ DialogAlert(props) {
216
+ return <DialogAlert {...props} />
217
+ },
218
+ DialogConfirm(props) {
219
+ return <DialogConfirm {...props} />
220
+ },
221
+ DialogPrompt(props) {
222
+ return <DialogPrompt {...props} description={props.description} />
223
+ },
224
+ DialogSelect(props) {
225
+ return (
226
+ <DialogSelect
227
+ title={props.title}
228
+ placeholder={props.placeholder}
229
+ options={props.options.map(mapOption)}
230
+ flat={props.flat}
231
+ onMove={mapOptionCb(props.onMove)}
232
+ onFilter={props.onFilter}
233
+ onSelect={mapOptionCb(props.onSelect)}
234
+ skipFilter={props.skipFilter}
235
+ current={props.current}
236
+ />
237
+ )
238
+ },
239
+ Slot<Name extends string>(props: TuiSlotProps<Name>) {
240
+ return <input.Slot {...props} />
241
+ },
242
+ Prompt(props) {
243
+ return (
244
+ <Prompt
245
+ sessionID={props.sessionID}
246
+ visible={props.visible}
247
+ disabled={props.disabled}
248
+ onSubmit={props.onSubmit}
249
+ ref={props.ref}
250
+ hint={props.hint}
251
+ right={props.right}
252
+ showPlaceholder={props.showPlaceholder}
253
+ placeholders={props.placeholders}
254
+ />
255
+ )
256
+ },
257
+ toast(inputToast) {
258
+ input.toast.show({
259
+ title: inputToast.title,
260
+ message: inputToast.message,
261
+ variant: inputToast.variant ?? "info",
262
+ duration: inputToast.duration,
263
+ })
264
+ },
265
+ dialog: {
266
+ replace(render, onClose) {
267
+ input.dialog.replace(render, onClose)
268
+ },
269
+ clear() {
270
+ input.dialog.clear()
271
+ },
272
+ setSize(size) {
273
+ input.dialog.setSize(size)
274
+ },
275
+ get size() {
276
+ return input.dialog.size
277
+ },
278
+ get depth() {
279
+ return input.dialog.stack.length
280
+ },
281
+ get open() {
282
+ return input.dialog.stack.length > 0
283
+ },
284
+ },
285
+ },
286
+ get tuiConfig() {
287
+ return input.tuiConfig
288
+ },
289
+ kv: {
290
+ get(key, fallback) {
291
+ return input.kv.get(key, fallback)
292
+ },
293
+ set(key, value) {
294
+ input.kv.set(key, value)
295
+ },
296
+ get ready() {
297
+ return input.kv.ready
298
+ },
299
+ },
300
+ state: stateApi(input.sync),
301
+ get client() {
302
+ return input.sdk.client
303
+ },
304
+ event: input.event,
305
+ renderer: input.renderer,
306
+ slots: {
307
+ register() {
308
+ throw new Error("slots.register is only available in plugin context")
309
+ },
310
+ },
311
+ plugins: {
312
+ list() {
313
+ return []
314
+ },
315
+ async activate() {
316
+ return false
317
+ },
318
+ async deactivate() {
319
+ return false
320
+ },
321
+ async add() {
322
+ return false
323
+ },
324
+ async install() {
325
+ return {
326
+ ok: false,
327
+ message: "plugins.install is only available in plugin context",
328
+ }
329
+ },
330
+ },
331
+ theme: {
332
+ get current() {
333
+ return input.theme.theme
334
+ },
335
+ get selected() {
336
+ return input.theme.selected
337
+ },
338
+ has(name) {
339
+ return input.theme.has(name)
340
+ },
341
+ set(name) {
342
+ return input.theme.set(name)
343
+ },
344
+ async install(_jsonPath) {
345
+ throw new Error("theme.install is only available in plugin context")
346
+ },
347
+ mode() {
348
+ return input.theme.mode()
349
+ },
350
+ get ready() {
351
+ return input.theme.ready
352
+ },
353
+ },
354
+ }
355
+ }
@@ -0,0 +1,52 @@
1
+ import type { TuiPluginApi, TuiRouteDefinition } from "@neurocode-ai/plugin/tui"
2
+ import { createSignal } from "solid-js"
3
+
4
+ type RouteEntry = {
5
+ key: symbol
6
+ render: TuiRouteDefinition["render"]
7
+ }
8
+
9
+ export type RouteMap = Map<string, RouteEntry[]>
10
+
11
+ export function createPluginRoutes() {
12
+ const routes: RouteMap = new Map()
13
+ const [revision, setRevision] = createSignal(0)
14
+
15
+ return {
16
+ register(list: TuiRouteDefinition[]) {
17
+ const key = Symbol()
18
+ list.forEach((item) => routes.set(item.name, [...(routes.get(item.name) ?? []), { key, render: item.render }]))
19
+ setRevision((value) => value + 1)
20
+
21
+ return () => {
22
+ list.forEach((item) => {
23
+ const next = routes.get(item.name)?.filter((entry) => entry.key !== key) ?? []
24
+ if (next.length) {
25
+ routes.set(item.name, next)
26
+ return
27
+ }
28
+ routes.delete(item.name)
29
+ })
30
+ setRevision((value) => value + 1)
31
+ }
32
+ },
33
+ get(name: string) {
34
+ revision()
35
+ return routes.get(name)?.at(-1)?.render
36
+ },
37
+ }
38
+ }
39
+
40
+ export type PluginRoutes = ReturnType<typeof createPluginRoutes>
41
+
42
+ export function createTuiApi(input: Omit<TuiPluginApi, "lifecycle">): TuiPluginApi {
43
+ return {
44
+ ...input,
45
+ lifecycle: {
46
+ signal: new AbortController().signal,
47
+ onDispose() {
48
+ return () => {}
49
+ },
50
+ },
51
+ }
52
+ }
@@ -0,0 +1,109 @@
1
+ // Legacy `api.command` bridge for v1 plugins; remove in v2.
2
+ import type { TuiCommand, TuiPluginApi } from "@neurocode-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,81 @@
1
+ import type {
2
+ TuiPluginApi,
3
+ TuiPluginInstallOptions,
4
+ TuiPluginInstallResult,
5
+ TuiPluginStatus,
6
+ } from "@neurocode-ai/plugin/tui"
7
+ import type { TuiConfig } from "../config"
8
+ import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js"
9
+ import { createPluginRoutes } from "./api"
10
+ import { createSlots, type HostSlots } from "./slots"
11
+
12
+ export function createPluginRuntime() {
13
+ const [commands, setCommands] = createSignal<PluginRuntimeCommands>(emptyCommands)
14
+ const [status, setStatus] = createSignal<ReadonlyArray<TuiPluginStatus>>([])
15
+ const slots = createSlots()
16
+
17
+ return {
18
+ Slot: slots.Slot,
19
+ routes: createPluginRoutes(),
20
+ commands,
21
+ status,
22
+ update(input: { commands?: PluginRuntimeCommands; status?: ReadonlyArray<TuiPluginStatus> }) {
23
+ if (input.commands) setCommands(input.commands)
24
+ if (input.status) setStatus(input.status)
25
+ },
26
+ clear() {
27
+ setCommands(emptyCommands)
28
+ setStatus([])
29
+ slots.clear()
30
+ },
31
+ setupSlots(api: TuiPluginApi): HostSlots {
32
+ return slots.setup(api)
33
+ },
34
+ }
35
+ }
36
+
37
+ export type PluginRuntimeCommands = {
38
+ activate: (id: string) => Promise<boolean>
39
+ deactivate: (id: string) => Promise<boolean>
40
+ add: (spec: string) => Promise<boolean>
41
+ install: (spec: string, options?: TuiPluginInstallOptions) => Promise<TuiPluginInstallResult>
42
+ }
43
+
44
+ const emptyCommands: PluginRuntimeCommands = {
45
+ async activate() {
46
+ return false
47
+ },
48
+ async deactivate() {
49
+ return false
50
+ },
51
+ async add() {
52
+ return false
53
+ },
54
+ async install() {
55
+ return { ok: false, message: "Plugin runtime is not available." }
56
+ },
57
+ }
58
+
59
+ export type PluginRuntime = ReturnType<typeof createPluginRuntime>
60
+
61
+ export type TuiPluginHost = {
62
+ start(input: {
63
+ api: TuiPluginApi
64
+ config: TuiConfig.Resolved
65
+ runtime: PluginRuntime
66
+ dispose?: () => void
67
+ }): Promise<void>
68
+ dispose(): Promise<void>
69
+ }
70
+
71
+ const Context = createContext<PluginRuntime>()
72
+
73
+ export function PluginRuntimeProvider(props: ParentProps<{ value: PluginRuntime }>): JSX.Element {
74
+ return <Context.Provider value={props.value}>{props.children}</Context.Provider>
75
+ }
76
+
77
+ export function usePluginRuntime() {
78
+ const runtime = useContext(Context)
79
+ if (!runtime) throw new Error("usePluginRuntime must be used within PluginRuntimeProvider")
80
+ return runtime
81
+ }
@@ -0,0 +1,65 @@
1
+ import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@neurocode-ai/plugin/tui"
2
+ import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid"
3
+ import { createSignal } from "solid-js"
4
+ import { isRecord } from "../util/record"
5
+
6
+ type RuntimeSlotMap = TuiSlotMap<Record<string, object>>
7
+ type SlotView = <Name extends string>(props: TuiSlotProps<Name>) => JSX.Element | null
8
+
9
+ export type HostSlotPlugin<Slots extends Record<string, object> = {}> = SolidPlugin<TuiSlotMap<Slots>, TuiSlotContext>
10
+ export type HostPluginApi = TuiPluginApi
11
+ export type HostSlots = {
12
+ register: {
13
+ (plugin: HostSlotPlugin): () => void
14
+ <Slots extends Record<string, object>>(plugin: HostSlotPlugin<Slots>): () => void
15
+ }
16
+ dispose: () => void
17
+ }
18
+
19
+ function isHostSlotPlugin(value: unknown): value is HostSlotPlugin<Record<string, object>> {
20
+ if (!isRecord(value)) return false
21
+ if (typeof value.id !== "string") return false
22
+ return isRecord(value.slots)
23
+ }
24
+
25
+ export function createSlots() {
26
+ const empty: SlotView = () => null
27
+ const [view, setView] = createSignal<SlotView>(empty)
28
+ const Slot: SlotView = (props) => view()(props)
29
+
30
+ return {
31
+ Slot,
32
+ setup(api: HostPluginApi): HostSlots {
33
+ const registry = createSolidSlotRegistry<RuntimeSlotMap, TuiSlotContext>(
34
+ api.renderer,
35
+ { theme: api.theme },
36
+ {
37
+ onPluginError(event) {
38
+ console.error("[tui.slot] plugin error", {
39
+ plugin: event.pluginId,
40
+ slot: event.slot,
41
+ phase: event.phase,
42
+ source: event.source,
43
+ message: event.error.message,
44
+ })
45
+ },
46
+ },
47
+ )
48
+ const slot = createSlot<RuntimeSlotMap, TuiSlotContext>(registry)
49
+ setView(() => (props: TuiSlotProps<string>) => slot(props))
50
+
51
+ return {
52
+ register(plugin: HostSlotPlugin) {
53
+ if (!isHostSlotPlugin(plugin)) return () => {}
54
+ return registry.register(plugin)
55
+ },
56
+ dispose() {
57
+ setView(() => empty)
58
+ },
59
+ }
60
+ },
61
+ clear() {
62
+ setView(() => empty)
63
+ },
64
+ }
65
+ }
@@ -0,0 +1,48 @@
1
+ const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
2
+
3
+ export function promptOffsetWidth(value: string) {
4
+ let width = 0
5
+ for (const part of graphemes.segment(value)) {
6
+ // Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.
7
+ width += part.segment === "\n" ? 1 : Bun.stringWidth(part.segment)
8
+ }
9
+ return width
10
+ }
11
+
12
+ function displayOffsetIndex(value: string, offset: number) {
13
+ if (offset <= 0) return 0
14
+
15
+ let width = 0
16
+ for (const part of graphemes.segment(value)) {
17
+ const next = width + promptOffsetWidth(part.segment)
18
+ if (next > offset) return part.index
19
+ width = next
20
+ }
21
+
22
+ return value.length
23
+ }
24
+
25
+ export function displaySlice(value: string, start = 0, end = promptOffsetWidth(value)) {
26
+ return value.slice(displayOffsetIndex(value, start), displayOffsetIndex(value, end))
27
+ }
28
+
29
+ export function displayCharAt(value: string, offset: number) {
30
+ let width = 0
31
+ for (const part of graphemes.segment(value)) {
32
+ const next = width + promptOffsetWidth(part.segment)
33
+ if (offset === width || offset < next) return part.segment
34
+ width = next
35
+ }
36
+ }
37
+
38
+ export function mentionTriggerIndex(value: string, offset = promptOffsetWidth(value)) {
39
+ const text = displaySlice(value, 0, offset)
40
+ const index = text.lastIndexOf("@")
41
+ if (index === -1) return
42
+
43
+ const before = index === 0 ? undefined : text[index - 1]
44
+ const query = text.slice(index)
45
+ if ((before === undefined || /\s/.test(before)) && !/\s/.test(query)) {
46
+ return promptOffsetWidth(text.slice(0, index))
47
+ }
48
+ }