@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.
- package/bunfig.toml +4 -0
- package/package.json +72 -0
- package/src/app.tsx +1134 -0
- package/src/attention.ts +260 -0
- package/src/audio.d.ts +9 -0
- package/src/audio.ts +53 -0
- package/src/clipboard.ts +124 -0
- package/src/component/bg-pulse-render.ts +436 -0
- package/src/component/bg-pulse.tsx +99 -0
- package/src/component/command-palette.tsx +79 -0
- package/src/component/dialog-agent.tsx +31 -0
- package/src/component/dialog-console-org.tsx +135 -0
- package/src/component/dialog-debug.tsx +90 -0
- package/src/component/dialog-mcp.tsx +85 -0
- package/src/component/dialog-model.tsx +197 -0
- package/src/component/dialog-move-session.tsx +353 -0
- package/src/component/dialog-provider.tsx +469 -0
- package/src/component/dialog-retry-action.tsx +160 -0
- package/src/component/dialog-session-delete-failed.tsx +99 -0
- package/src/component/dialog-session-list.tsx +364 -0
- package/src/component/dialog-session-rename.tsx +31 -0
- package/src/component/dialog-skill.tsx +70 -0
- package/src/component/dialog-stash.tsx +87 -0
- package/src/component/dialog-status.tsx +168 -0
- package/src/component/dialog-tag.tsx +47 -0
- package/src/component/dialog-theme-list.tsx +50 -0
- package/src/component/dialog-variant.tsx +39 -0
- package/src/component/dialog-workspace-create.tsx +308 -0
- package/src/component/dialog-workspace-file-changes.tsx +144 -0
- package/src/component/dialog-workspace-list.tsx +112 -0
- package/src/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/component/error-component.tsx +240 -0
- package/src/component/logo.tsx +61 -0
- package/src/component/plugin-route-missing.tsx +14 -0
- package/src/component/prompt/autocomplete.tsx +781 -0
- package/src/component/prompt/cwd.ts +0 -0
- package/src/component/prompt/frecency.tsx +1 -0
- package/src/component/prompt/history.tsx +1 -0
- package/src/component/prompt/index.tsx +1713 -0
- package/src/component/prompt/local-attachment.ts +48 -0
- package/src/component/prompt/move.tsx +205 -0
- package/src/component/prompt/stash.tsx +1 -0
- package/src/component/prompt/workspace.tsx +137 -0
- package/src/component/register-spinner.ts +6 -0
- package/src/component/spinner.tsx +26 -0
- package/src/component/startup-loading.tsx +63 -0
- package/src/component/todo-item.tsx +32 -0
- package/src/component/use-connected.tsx +12 -0
- package/src/component/workspace-label.tsx +19 -0
- package/src/config/index.tsx +129 -0
- package/src/config/keybind.ts +471 -0
- package/src/context/args.tsx +16 -0
- package/src/context/clipboard.tsx +18 -0
- package/src/context/data.tsx +569 -0
- package/src/context/directory.ts +17 -0
- package/src/context/editor.ts +408 -0
- package/src/context/epilogue.tsx +6 -0
- package/src/context/event.ts +36 -0
- package/src/context/exit.tsx +8 -0
- package/src/context/helper.tsx +26 -0
- package/src/context/kv.tsx +66 -0
- package/src/context/local.tsx +542 -0
- package/src/context/location.tsx +14 -0
- package/src/context/path-format.tsx +24 -0
- package/src/context/permission.tsx +26 -0
- package/src/context/project.tsx +115 -0
- package/src/context/prompt.tsx +18 -0
- package/src/context/route.tsx +60 -0
- package/src/context/runtime.tsx +62 -0
- package/src/context/sdk.tsx +151 -0
- package/src/context/sync.tsx +666 -0
- package/src/context/theme.tsx +332 -0
- package/src/context/thinking.ts +67 -0
- package/src/editor-zed.ts +286 -0
- package/src/editor.ts +101 -0
- package/src/feature-plugins/builtins.ts +36 -0
- package/src/feature-plugins/home/footer.tsx +100 -0
- package/src/feature-plugins/home/tips-view.tsx +287 -0
- package/src/feature-plugins/home/tips.tsx +59 -0
- package/src/feature-plugins/sidebar/context.tsx +65 -0
- package/src/feature-plugins/sidebar/files.tsx +70 -0
- package/src/feature-plugins/sidebar/footer.tsx +98 -0
- package/src/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
- package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
- package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
- package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
- package/src/feature-plugins/system/notifications.ts +94 -0
- package/src/feature-plugins/system/plugins.tsx +269 -0
- package/src/feature-plugins/system/which-key.tsx +608 -0
- package/src/index.tsx +1 -0
- package/src/keymap.tsx +290 -0
- package/src/logo.ts +11 -0
- package/src/parsers-config.ts +386 -0
- package/src/plugin/adapters.tsx +355 -0
- package/src/plugin/api.ts +52 -0
- package/src/plugin/command-shim.ts +109 -0
- package/src/plugin/runtime.tsx +81 -0
- package/src/plugin/slots.tsx +65 -0
- package/src/prompt/display.ts +48 -0
- package/src/prompt/frecency.tsx +80 -0
- package/src/prompt/history.tsx +111 -0
- package/src/prompt/part.ts +29 -0
- package/src/prompt/stash.tsx +89 -0
- package/src/prompt/traits.ts +29 -0
- package/src/routes/home/session-destination.tsx +41 -0
- package/src/routes/home.tsx +95 -0
- package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/routes/session/dialog-message.tsx +109 -0
- package/src/routes/session/dialog-subagent.tsx +26 -0
- package/src/routes/session/dialog-timeline.tsx +47 -0
- package/src/routes/session/footer.tsx +91 -0
- package/src/routes/session/index.tsx +2710 -0
- package/src/routes/session/permission.tsx +718 -0
- package/src/routes/session/question.tsx +514 -0
- package/src/routes/session/sidebar.tsx +103 -0
- package/src/routes/session/subagent-footer.tsx +132 -0
- package/src/runtime.tsx +9 -0
- package/src/terminal-win32.ts +130 -0
- package/src/theme/assets/aura.json +69 -0
- package/src/theme/assets/ayu.json +80 -0
- package/src/theme/assets/carbonfox.json +248 -0
- package/src/theme/assets/catppuccin-frappe.json +230 -0
- package/src/theme/assets/catppuccin-macchiato.json +230 -0
- package/src/theme/assets/catppuccin.json +112 -0
- package/src/theme/assets/cobalt2.json +225 -0
- package/src/theme/assets/cursor.json +249 -0
- package/src/theme/assets/dracula.json +219 -0
- package/src/theme/assets/everforest.json +241 -0
- package/src/theme/assets/flexoki.json +237 -0
- package/src/theme/assets/github.json +233 -0
- package/src/theme/assets/gruvbox.json +242 -0
- package/src/theme/assets/kanagawa.json +77 -0
- package/src/theme/assets/lucent-orng.json +234 -0
- package/src/theme/assets/material.json +235 -0
- package/src/theme/assets/matrix.json +77 -0
- package/src/theme/assets/mercury.json +252 -0
- package/src/theme/assets/monokai.json +221 -0
- package/src/theme/assets/nightowl.json +221 -0
- package/src/theme/assets/nord.json +223 -0
- package/src/theme/assets/one-dark.json +84 -0
- package/src/theme/assets/opencode.json +245 -0
- package/src/theme/assets/orng.json +249 -0
- package/src/theme/assets/osaka-jade.json +93 -0
- package/src/theme/assets/palenight.json +222 -0
- package/src/theme/assets/rosepine.json +234 -0
- package/src/theme/assets/solarized.json +223 -0
- package/src/theme/assets/synthwave84.json +226 -0
- package/src/theme/assets/tokyonight.json +243 -0
- package/src/theme/assets/vercel.json +245 -0
- package/src/theme/assets/vesper.json +218 -0
- package/src/theme/assets/zenburn.json +223 -0
- package/src/theme/index.ts +1089 -0
- package/src/ui/border.ts +21 -0
- package/src/ui/dialog-alert.tsx +66 -0
- package/src/ui/dialog-confirm.tsx +108 -0
- package/src/ui/dialog-export-options.tsx +217 -0
- package/src/ui/dialog-help.tsx +40 -0
- package/src/ui/dialog-prompt.tsx +126 -0
- package/src/ui/dialog-select.tsx +790 -0
- package/src/ui/dialog.tsx +231 -0
- package/src/ui/link.tsx +34 -0
- package/src/ui/spinner.ts +368 -0
- package/src/ui/toast.tsx +102 -0
- package/src/util/collapse-tool-output.ts +19 -0
- package/src/util/error.ts +182 -0
- package/src/util/filetype.ts +130 -0
- package/src/util/format.ts +20 -0
- package/src/util/layout.ts +25 -0
- package/src/util/locale.ts +86 -0
- package/src/util/model.ts +28 -0
- package/src/util/path.ts +12 -0
- package/src/util/persistence.ts +33 -0
- package/src/util/presentation.ts +38 -0
- package/src/util/provider-origin.ts +7 -0
- package/src/util/record.ts +3 -0
- package/src/util/renderer.ts +7 -0
- package/src/util/revert-diff.ts +18 -0
- package/src/util/scroll.ts +27 -0
- package/src/util/selection.ts +79 -0
- package/src/util/session.ts +3 -0
- package/src/util/signal.ts +51 -0
- package/src/util/system.ts +20 -0
- package/src/util/tool-display.ts +13 -0
- package/src/util/transcript.ts +112 -0
- package/sst-env.d.ts +10 -0
- package/test/app-lifecycle.test.tsx +128 -0
- package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
- package/test/cli/cmd/tui/model-options.test.ts +32 -0
- package/test/cli/cmd/tui/notifications.test.ts +267 -0
- package/test/cli/cmd/tui/provider-options.test.ts +41 -0
- package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
- package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
- package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
- package/test/cli/cmd/tui/sync.test.tsx +65 -0
- package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
- package/test/cli/tui/data.test.tsx +486 -0
- package/test/cli/tui/dialog-prompt.test.tsx +147 -0
- package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
- package/test/cli/tui/diff-viewer.test.tsx +268 -0
- package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
- package/test/cli/tui/prompt-submit-race.test.ts +98 -0
- package/test/cli/tui/thinking.test.ts +36 -0
- package/test/cli/tui/use-event.test.tsx +148 -0
- package/test/clipboard.test.ts +19 -0
- package/test/component/dialog-session-list.test.ts +46 -0
- package/test/config.test.tsx +127 -0
- package/test/context/local.test.ts +22 -0
- package/test/editor.test.ts +32 -0
- package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
- package/test/fixture/fixture.ts +13 -0
- package/test/fixture/tui-environment.tsx +32 -0
- package/test/fixture/tui-plugin.ts +36 -0
- package/test/fixture/tui-runtime.ts +12 -0
- package/test/fixture/tui-sdk.ts +109 -0
- package/test/index.test.tsx +6 -0
- package/test/keymap.test.tsx +141 -0
- package/test/plugin/runtime.test.ts +50 -0
- package/test/plugin/slots.test.tsx +38 -0
- package/test/prompt/display.test.ts +33 -0
- package/test/prompt/history.test.ts +39 -0
- package/test/prompt/jsonl.test.ts +24 -0
- package/test/prompt/local-attachment.test.ts +43 -0
- package/test/prompt/part.test.ts +53 -0
- package/test/prompt/persistence.test.ts +23 -0
- package/test/prompt/traits.test.ts +25 -0
- package/test/runtime.test.tsx +37 -0
- package/test/theme.test.ts +81 -0
- package/test/util/error.test.ts +49 -0
- package/test/util/filetype.test.ts +16 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/model.test.ts +9 -0
- package/test/util/presentation.test.ts +8 -0
- package/test/util/renderer.test.ts +30 -0
- package/test/util/revert-diff.test.ts +35 -0
- package/test/util/session.test.ts +10 -0
- package/test/util/tool-display.test.ts +40 -0
- package/test/util/transcript.test.ts +421 -0
- 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
|
+
}
|