@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,70 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
3
|
+
import { createMemo, For, Show, createSignal } from "solid-js"
|
|
4
|
+
import { Locale } from "../../util/locale"
|
|
5
|
+
|
|
6
|
+
const id = "internal:sidebar-files"
|
|
7
|
+
|
|
8
|
+
function changeCountWidth(item: { additions: number; deletions: number }) {
|
|
9
|
+
return [item.additions ? `+${item.additions}` : "", item.deletions ? `-${item.deletions}` : ""]
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join(" ").length
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
15
|
+
const [open, setOpen] = createSignal(true)
|
|
16
|
+
const theme = () => props.api.theme.current
|
|
17
|
+
const list = createMemo(() => props.api.state.session.diff(props.session_id))
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Show when={list().length > 0}>
|
|
21
|
+
<box>
|
|
22
|
+
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
|
23
|
+
<Show when={list().length > 2}>
|
|
24
|
+
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
|
25
|
+
</Show>
|
|
26
|
+
<text fg={theme().text}>
|
|
27
|
+
<b>Modified Files</b>
|
|
28
|
+
</text>
|
|
29
|
+
</box>
|
|
30
|
+
<Show when={list().length <= 2 || open()}>
|
|
31
|
+
<For each={list()}>
|
|
32
|
+
{(item) => (
|
|
33
|
+
<box flexDirection="row" gap={1} justifyContent="space-between">
|
|
34
|
+
<text fg={theme().textMuted} wrapMode="none">
|
|
35
|
+
{Locale.truncateLeft(item.file, Math.max(2, 36 - changeCountWidth(item)))}
|
|
36
|
+
</text>
|
|
37
|
+
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
38
|
+
<Show when={item.additions}>
|
|
39
|
+
<text fg={theme().diffAdded}>+{item.additions}</text>
|
|
40
|
+
</Show>
|
|
41
|
+
<Show when={item.deletions}>
|
|
42
|
+
<text fg={theme().diffRemoved}>-{item.deletions}</text>
|
|
43
|
+
</Show>
|
|
44
|
+
</box>
|
|
45
|
+
</box>
|
|
46
|
+
)}
|
|
47
|
+
</For>
|
|
48
|
+
</Show>
|
|
49
|
+
</box>
|
|
50
|
+
</Show>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tui: TuiPlugin = async (api) => {
|
|
55
|
+
api.slots.register({
|
|
56
|
+
order: 500,
|
|
57
|
+
slots: {
|
|
58
|
+
sidebar_content(_ctx, props) {
|
|
59
|
+
return <View api={api} session_id={props.session_id} />
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const plugin: BuiltinTuiPlugin = {
|
|
66
|
+
id,
|
|
67
|
+
tui,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default plugin
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
3
|
+
import { createMemo, Show } from "solid-js"
|
|
4
|
+
import { abbreviateHome } from "../../runtime"
|
|
5
|
+
import { useTuiPaths } from "../../context/runtime"
|
|
6
|
+
|
|
7
|
+
const id = "internal:sidebar-footer"
|
|
8
|
+
|
|
9
|
+
function View(props: { api: TuiPluginApi; sessionID: string }) {
|
|
10
|
+
const paths = useTuiPaths()
|
|
11
|
+
const theme = () => props.api.theme.current
|
|
12
|
+
const has = createMemo(() =>
|
|
13
|
+
props.api.state.provider.some(
|
|
14
|
+
(item) => item.id !== "opencode" && item.id !== "neuro" || Object.values(item.models).some((model) => model.cost?.input !== 0),
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
const done = createMemo(() => props.api.kv.get("dismissed_getting_started", false))
|
|
18
|
+
const show = createMemo(() => !has() && !done())
|
|
19
|
+
const path = createMemo(() => {
|
|
20
|
+
const session = props.api.state.session.get(props.sessionID)
|
|
21
|
+
const dir = session?.directory || props.api.state.path.directory || paths.cwd
|
|
22
|
+
const out = abbreviateHome(dir, paths.home)
|
|
23
|
+
const branch = session?.directory === props.api.state.path.directory ? props.api.state.vcs?.branch : undefined
|
|
24
|
+
const text = branch ? out + ":" + branch : out
|
|
25
|
+
const list = text.split("/")
|
|
26
|
+
return {
|
|
27
|
+
parent: list.slice(0, -1).join("/"),
|
|
28
|
+
name: list.at(-1) ?? "",
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<box gap={1}>
|
|
34
|
+
<Show when={show()}>
|
|
35
|
+
<box
|
|
36
|
+
backgroundColor={theme().backgroundElement}
|
|
37
|
+
paddingTop={1}
|
|
38
|
+
paddingBottom={1}
|
|
39
|
+
paddingLeft={2}
|
|
40
|
+
paddingRight={2}
|
|
41
|
+
flexDirection="row"
|
|
42
|
+
gap={1}
|
|
43
|
+
>
|
|
44
|
+
<text flexShrink={0} fg={theme().text}>
|
|
45
|
+
⬖
|
|
46
|
+
</text>
|
|
47
|
+
<box flexGrow={1} gap={1}>
|
|
48
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
49
|
+
<text fg={theme().text}>
|
|
50
|
+
<b>Getting started</b>
|
|
51
|
+
</text>
|
|
52
|
+
<text fg={theme().textMuted} onMouseDown={() => props.api.kv.set("dismissed_getting_started", true)}>
|
|
53
|
+
✕
|
|
54
|
+
</text>
|
|
55
|
+
</box>
|
|
56
|
+
<text fg={theme().textMuted}>NeuroCode includes free models so you can start immediately.</text>
|
|
57
|
+
<text fg={theme().textMuted}>
|
|
58
|
+
Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
|
|
59
|
+
</text>
|
|
60
|
+
<box flexDirection="row" gap={1} justifyContent="space-between">
|
|
61
|
+
<text fg={theme().text}>Connect provider</text>
|
|
62
|
+
<text fg={theme().textMuted}>/connect</text>
|
|
63
|
+
</box>
|
|
64
|
+
</box>
|
|
65
|
+
</box>
|
|
66
|
+
</Show>
|
|
67
|
+
<text>
|
|
68
|
+
<span style={{ fg: theme().textMuted }}>{path().parent}/</span>
|
|
69
|
+
<span style={{ fg: theme().text }}>{path().name}</span>
|
|
70
|
+
</text>
|
|
71
|
+
<text fg={theme().textMuted}>
|
|
72
|
+
<span style={{ fg: theme().success }}>•</span> <b>Neuro</b>
|
|
73
|
+
<span style={{ fg: theme().text }}>
|
|
74
|
+
<b>Code</b>
|
|
75
|
+
</span>{" "}
|
|
76
|
+
<span>{props.api.app.version}</span>
|
|
77
|
+
</text>
|
|
78
|
+
</box>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tui: TuiPlugin = async (api) => {
|
|
83
|
+
api.slots.register({
|
|
84
|
+
order: 100,
|
|
85
|
+
slots: {
|
|
86
|
+
sidebar_footer(_ctx, props) {
|
|
87
|
+
return <View api={api} sessionID={props.session_id} />
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const plugin: BuiltinTuiPlugin = {
|
|
94
|
+
id,
|
|
95
|
+
tui,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default plugin
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
3
|
+
import { createMemo, For, Show, createSignal } from "solid-js"
|
|
4
|
+
|
|
5
|
+
const id = "internal:sidebar-lsp"
|
|
6
|
+
|
|
7
|
+
function View(props: { api: TuiPluginApi }) {
|
|
8
|
+
const [open, setOpen] = createSignal(true)
|
|
9
|
+
const theme = () => props.api.theme.current
|
|
10
|
+
const list = createMemo(() => props.api.state.lsp())
|
|
11
|
+
const off = createMemo(() => !props.api.state.config.lsp)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<box>
|
|
15
|
+
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
|
16
|
+
<Show when={list().length > 2}>
|
|
17
|
+
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
|
18
|
+
</Show>
|
|
19
|
+
<text fg={theme().text}>
|
|
20
|
+
<b>LSP</b>
|
|
21
|
+
</text>
|
|
22
|
+
</box>
|
|
23
|
+
<Show when={list().length <= 2 || open()}>
|
|
24
|
+
<Show when={list().length === 0}>
|
|
25
|
+
<text fg={theme().textMuted}>{off() ? "LSPs are disabled" : "LSPs will activate as files are read"}</text>
|
|
26
|
+
</Show>
|
|
27
|
+
<For each={list()}>
|
|
28
|
+
{(item) => (
|
|
29
|
+
<box flexDirection="row" gap={1}>
|
|
30
|
+
<text
|
|
31
|
+
flexShrink={0}
|
|
32
|
+
style={{
|
|
33
|
+
fg: item.status === "connected" ? theme().success : theme().error,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
•
|
|
37
|
+
</text>
|
|
38
|
+
<text fg={theme().textMuted}>
|
|
39
|
+
{item.id} {item.root}
|
|
40
|
+
</text>
|
|
41
|
+
</box>
|
|
42
|
+
)}
|
|
43
|
+
</For>
|
|
44
|
+
</Show>
|
|
45
|
+
</box>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tui: TuiPlugin = async (api) => {
|
|
50
|
+
api.slots.register({
|
|
51
|
+
order: 300,
|
|
52
|
+
slots: {
|
|
53
|
+
sidebar_content() {
|
|
54
|
+
return <View api={api} />
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const plugin: BuiltinTuiPlugin = {
|
|
61
|
+
id,
|
|
62
|
+
tui,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default plugin
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
3
|
+
import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js"
|
|
4
|
+
|
|
5
|
+
const id = "internal:sidebar-mcp"
|
|
6
|
+
|
|
7
|
+
function View(props: { api: TuiPluginApi }) {
|
|
8
|
+
const [open, setOpen] = createSignal(true)
|
|
9
|
+
const theme = () => props.api.theme.current
|
|
10
|
+
const list = createMemo(() => props.api.state.mcp())
|
|
11
|
+
const on = createMemo(() => list().filter((item) => item.status === "connected").length)
|
|
12
|
+
const bad = createMemo(
|
|
13
|
+
() =>
|
|
14
|
+
list().filter(
|
|
15
|
+
(item) =>
|
|
16
|
+
item.status === "failed" || item.status === "needs_auth" || item.status === "needs_client_registration",
|
|
17
|
+
).length,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const dot = (status: string) => {
|
|
21
|
+
if (status === "connected") return theme().success
|
|
22
|
+
if (status === "failed") return theme().error
|
|
23
|
+
if (status === "disabled") return theme().textMuted
|
|
24
|
+
if (status === "needs_auth") return theme().warning
|
|
25
|
+
if (status === "needs_client_registration") return theme().error
|
|
26
|
+
return theme().textMuted
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Show when={list().length > 0}>
|
|
31
|
+
<box>
|
|
32
|
+
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
|
33
|
+
<Show when={list().length > 2}>
|
|
34
|
+
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
|
35
|
+
</Show>
|
|
36
|
+
<text fg={theme().text}>
|
|
37
|
+
<b>MCP</b>
|
|
38
|
+
<Show when={!open()}>
|
|
39
|
+
<span style={{ fg: theme().textMuted }}>
|
|
40
|
+
{" "}
|
|
41
|
+
({on()} active{bad() > 0 ? `, ${bad()} error${bad() > 1 ? "s" : ""}` : ""})
|
|
42
|
+
</span>
|
|
43
|
+
</Show>
|
|
44
|
+
</text>
|
|
45
|
+
</box>
|
|
46
|
+
<Show when={list().length <= 2 || open()}>
|
|
47
|
+
<For each={list()}>
|
|
48
|
+
{(item) => (
|
|
49
|
+
<box flexDirection="row" gap={1}>
|
|
50
|
+
<text
|
|
51
|
+
flexShrink={0}
|
|
52
|
+
style={{
|
|
53
|
+
fg: dot(item.status),
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
•
|
|
57
|
+
</text>
|
|
58
|
+
<text fg={theme().text} wrapMode="word">
|
|
59
|
+
{item.name}{" "}
|
|
60
|
+
<span style={{ fg: theme().textMuted }}>
|
|
61
|
+
<Switch fallback={item.status}>
|
|
62
|
+
<Match when={item.status === "connected"}>Connected</Match>
|
|
63
|
+
<Match when={item.status === "failed"}>
|
|
64
|
+
<i>{item.error}</i>
|
|
65
|
+
</Match>
|
|
66
|
+
<Match when={item.status === "disabled"}>Disabled</Match>
|
|
67
|
+
<Match when={item.status === "needs_auth"}>Needs auth</Match>
|
|
68
|
+
<Match when={item.status === "needs_client_registration"}>Needs client ID</Match>
|
|
69
|
+
</Switch>
|
|
70
|
+
</span>
|
|
71
|
+
</text>
|
|
72
|
+
</box>
|
|
73
|
+
)}
|
|
74
|
+
</For>
|
|
75
|
+
</Show>
|
|
76
|
+
</box>
|
|
77
|
+
</Show>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tui: TuiPlugin = async (api) => {
|
|
82
|
+
api.slots.register({
|
|
83
|
+
order: 200,
|
|
84
|
+
slots: {
|
|
85
|
+
sidebar_content() {
|
|
86
|
+
return <View api={api} />
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const plugin: BuiltinTuiPlugin = {
|
|
93
|
+
id,
|
|
94
|
+
tui,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default plugin
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
3
|
+
import { createMemo, For, Show, createSignal } from "solid-js"
|
|
4
|
+
import { TodoItem } from "../../component/todo-item"
|
|
5
|
+
|
|
6
|
+
const id = "internal:sidebar-todo"
|
|
7
|
+
|
|
8
|
+
function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
9
|
+
const [open, setOpen] = createSignal(true)
|
|
10
|
+
const theme = () => props.api.theme.current
|
|
11
|
+
const list = createMemo(() => props.api.state.session.todo(props.session_id))
|
|
12
|
+
const show = createMemo(() => list().length > 0 && list().some((item) => item.status !== "completed"))
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Show when={show()}>
|
|
16
|
+
<box>
|
|
17
|
+
<box flexDirection="row" gap={1} onMouseDown={() => list().length > 2 && setOpen((x) => !x)}>
|
|
18
|
+
<Show when={list().length > 2}>
|
|
19
|
+
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
|
|
20
|
+
</Show>
|
|
21
|
+
<text fg={theme().text}>
|
|
22
|
+
<b>Todo</b>
|
|
23
|
+
</text>
|
|
24
|
+
</box>
|
|
25
|
+
<Show when={list().length <= 2 || open()}>
|
|
26
|
+
<For each={list()}>{(item) => <TodoItem status={item.status} content={item.content} />}</For>
|
|
27
|
+
</Show>
|
|
28
|
+
</box>
|
|
29
|
+
</Show>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tui: TuiPlugin = async (api) => {
|
|
34
|
+
api.slots.register({
|
|
35
|
+
order: 400,
|
|
36
|
+
slots: {
|
|
37
|
+
sidebar_content(_ctx, props) {
|
|
38
|
+
return <View api={api} session_id={props.session_id} />
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const plugin: BuiltinTuiPlugin = {
|
|
45
|
+
id,
|
|
46
|
+
tui,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default plugin
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Paths branch softly through the screen,
|
|
2
|
+
// A quiet tree of changed designs;
|
|
3
|
+
// Each leaf remembers what has been,
|
|
4
|
+
// And waits where careful light aligns.
|
|
5
|
+
|
|
6
|
+
export type FileTreeItem = {
|
|
7
|
+
readonly file: string
|
|
8
|
+
readonly status?: "added" | "deleted" | "modified"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FileTreeNode = {
|
|
12
|
+
readonly id: number
|
|
13
|
+
readonly name: string
|
|
14
|
+
readonly parent: number | undefined
|
|
15
|
+
readonly children: number[]
|
|
16
|
+
readonly depth: number
|
|
17
|
+
readonly kind: "directory" | "file"
|
|
18
|
+
readonly fileIndex?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FileTree = {
|
|
22
|
+
readonly roots: number[]
|
|
23
|
+
readonly nodes: FileTreeNode[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FileTreeRow = {
|
|
27
|
+
readonly id: number
|
|
28
|
+
readonly depth: number
|
|
29
|
+
readonly kind: "directory" | "file"
|
|
30
|
+
readonly name: string
|
|
31
|
+
readonly fileIndex?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildFileTree(files: readonly FileTreeItem[]): FileTree {
|
|
35
|
+
const roots: number[] = []
|
|
36
|
+
const nodes: FileTreeNode[] = []
|
|
37
|
+
const directoryByPath = new Map<string, number>()
|
|
38
|
+
|
|
39
|
+
files.forEach((file, fileIndex) => {
|
|
40
|
+
const segments = file.file.split("/").filter(Boolean)
|
|
41
|
+
if (segments.length === 0) return
|
|
42
|
+
|
|
43
|
+
const parent = segments.slice(0, -1).reduce(
|
|
44
|
+
(state, segment) => {
|
|
45
|
+
const directoryPath = state.path ? `${state.path}/${segment}` : segment
|
|
46
|
+
const existing = directoryByPath.get(directoryPath)
|
|
47
|
+
if (existing !== undefined) return { id: existing, path: directoryPath, depth: state.depth + 1 }
|
|
48
|
+
|
|
49
|
+
const id = addFileTreeNode(nodes, roots, {
|
|
50
|
+
name: segment,
|
|
51
|
+
parent: state.id,
|
|
52
|
+
depth: state.depth,
|
|
53
|
+
kind: "directory",
|
|
54
|
+
})
|
|
55
|
+
directoryByPath.set(directoryPath, id)
|
|
56
|
+
return { id, path: directoryPath, depth: state.depth + 1 }
|
|
57
|
+
},
|
|
58
|
+
{ id: undefined as number | undefined, path: "", depth: 0 },
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
addFileTreeNode(nodes, roots, {
|
|
62
|
+
name: segments[segments.length - 1]!,
|
|
63
|
+
parent: parent.id,
|
|
64
|
+
depth: parent.depth,
|
|
65
|
+
kind: "file",
|
|
66
|
+
fileIndex,
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const tree = { roots, nodes }
|
|
71
|
+
tree.roots.sort((left, right) => compareFileTreeNodes(tree, left, right))
|
|
72
|
+
tree.nodes.forEach((node) => node.children.sort((left, right) => compareFileTreeNodes(tree, left, right)))
|
|
73
|
+
return tree
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function flattenFileTree(tree: FileTree, expanded?: ReadonlySet<number>): FileTreeRow[] {
|
|
77
|
+
const rows: FileTreeRow[] = []
|
|
78
|
+
const visit = (id: number, depth: number) => {
|
|
79
|
+
const node = tree.nodes[id]!
|
|
80
|
+
if (node.kind === "file") {
|
|
81
|
+
rows.push({
|
|
82
|
+
id: node.id,
|
|
83
|
+
depth,
|
|
84
|
+
kind: node.kind,
|
|
85
|
+
name: node.name,
|
|
86
|
+
fileIndex: node.fileIndex,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const chain = collapsedFileTreeDirectoryChain(tree, node.id)
|
|
92
|
+
const last = chain[chain.length - 1]!
|
|
93
|
+
rows.push({
|
|
94
|
+
id: node.id,
|
|
95
|
+
depth,
|
|
96
|
+
kind: node.kind,
|
|
97
|
+
name: chain.map((item) => item.name).join("/"),
|
|
98
|
+
fileIndex: node.fileIndex,
|
|
99
|
+
})
|
|
100
|
+
if (!expanded || expanded.has(node.id)) last.children.forEach((child) => visit(child, depth + 1))
|
|
101
|
+
}
|
|
102
|
+
tree.roots.forEach((root) => visit(root, 0))
|
|
103
|
+
return rows
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collapsedFileTreeDirectoryChain(tree: FileTree, id: number): FileTreeNode[] {
|
|
107
|
+
const node = tree.nodes[id]!
|
|
108
|
+
const child = node.children.length === 1 ? tree.nodes[node.children[0]!] : undefined
|
|
109
|
+
if (child?.kind !== "directory") return [node]
|
|
110
|
+
return [node, ...collapsedFileTreeDirectoryChain(tree, child.id)]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function compareFileTreeNodes(tree: FileTree, left: number, right: number) {
|
|
114
|
+
const leftNode = tree.nodes[left]!
|
|
115
|
+
const rightNode = tree.nodes[right]!
|
|
116
|
+
if (leftNode.kind !== rightNode.kind) return leftNode.kind === "directory" ? -1 : 1
|
|
117
|
+
if (leftNode.name < rightNode.name) return -1
|
|
118
|
+
if (leftNode.name > rightNode.name) return 1
|
|
119
|
+
return left - right
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function moveFileTreeSelection(rows: readonly FileTreeRow[], selected: number | undefined, offset: number) {
|
|
123
|
+
if (rows.length === 0) return undefined
|
|
124
|
+
const index = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
|
|
125
|
+
if (index === -1) return rows[0]!.id
|
|
126
|
+
return rows[Math.max(0, Math.min(rows.length - 1, index + offset))]!.id
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function moveFileTreeSelectionToFirstChild(rows: readonly FileTreeRow[], selected: number | undefined) {
|
|
130
|
+
const index = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
|
|
131
|
+
const row = index === -1 ? undefined : rows[index]
|
|
132
|
+
if (row?.kind !== "directory") return selected
|
|
133
|
+
const child = rows[index + 1]
|
|
134
|
+
return child && child.depth > row.depth ? child.id : selected
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function moveFileTreeSelectionToParent(rows: readonly FileTreeRow[], selected: number | undefined) {
|
|
138
|
+
const index = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
|
|
139
|
+
const row = index === -1 ? undefined : rows[index]
|
|
140
|
+
if (!row || row.depth === 0) return selected
|
|
141
|
+
return rows.findLast((item, itemIndex) => itemIndex < index && item.depth < row.depth)?.id ?? selected
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function moveFileTreeSelectionToFile(
|
|
145
|
+
rows: readonly FileTreeRow[],
|
|
146
|
+
selected: number | undefined,
|
|
147
|
+
offset: number,
|
|
148
|
+
) {
|
|
149
|
+
const fileRows = rows.filter((row) => row.fileIndex !== undefined)
|
|
150
|
+
if (fileRows.length === 0) return undefined
|
|
151
|
+
const selectedIndex = selected === undefined ? -1 : rows.findIndex((row) => row.id === selected)
|
|
152
|
+
if (selectedIndex === -1) return offset < 0 ? fileRows[fileRows.length - 1]!.id : fileRows[0]!.id
|
|
153
|
+
const next =
|
|
154
|
+
offset < 0
|
|
155
|
+
? fileRows.findLast((row) => rows.findIndex((item) => item.id === row.id) < selectedIndex)
|
|
156
|
+
: fileRows.find((row) => rows.findIndex((item) => item.id === row.id) > selectedIndex)
|
|
157
|
+
return next?.id ?? (offset < 0 ? fileRows[0]!.id : fileRows[fileRows.length - 1]!.id)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function fileTreeFileSelection(tree: FileTree, fileIndex: number) {
|
|
161
|
+
const node = tree.nodes.find((item) => item.kind === "file" && item.fileIndex === fileIndex)
|
|
162
|
+
if (!node) return undefined
|
|
163
|
+
return {
|
|
164
|
+
highlightedNode: node.id,
|
|
165
|
+
expandedNodes: fileTreeParentDirectories(tree, node.id),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function singlePatchFileIndex(
|
|
170
|
+
selected: number | undefined,
|
|
171
|
+
active: number | undefined,
|
|
172
|
+
current: number | undefined,
|
|
173
|
+
first: number | undefined,
|
|
174
|
+
) {
|
|
175
|
+
return selected ?? active ?? current ?? first
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function orderedPatchFileIndexes(rows: readonly FileTreeRow[]) {
|
|
179
|
+
return rows.flatMap((row) => (row.fileIndex === undefined ? [] : [row.fileIndex]))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function showDiffViewerFileTree(showFileTree: boolean, fileCount: number) {
|
|
183
|
+
return showFileTree && fileCount > 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function movePatchFileIndex(fileIndexes: readonly number[], current: number | undefined, offset: number) {
|
|
187
|
+
if (fileIndexes.length === 0) return undefined
|
|
188
|
+
const index = current === undefined ? -1 : fileIndexes.indexOf(current)
|
|
189
|
+
if (index === -1) return fileIndexes[0]
|
|
190
|
+
return fileIndexes[Math.max(0, Math.min(fileIndexes.length - 1, index + offset))]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function allExpandedFileTreeDirectories(tree: FileTree) {
|
|
194
|
+
return new Set(tree.nodes.filter((node) => node.kind === "directory").map((node) => node.id))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function toggleFileTreeDirectory(tree: FileTree, expanded: ReadonlySet<number>, selected: number | undefined) {
|
|
198
|
+
if (selected === undefined || tree.nodes[selected]?.kind !== "directory") return expanded
|
|
199
|
+
const next = new Set(expanded)
|
|
200
|
+
if (next.has(selected)) next.delete(selected)
|
|
201
|
+
else next.add(selected)
|
|
202
|
+
return next
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function setFileTreeDirectoryExpanded(
|
|
206
|
+
tree: FileTree,
|
|
207
|
+
expanded: ReadonlySet<number>,
|
|
208
|
+
selected: number | undefined,
|
|
209
|
+
value: boolean,
|
|
210
|
+
) {
|
|
211
|
+
if (selected === undefined || tree.nodes[selected]?.kind !== "directory") return expanded
|
|
212
|
+
const next = new Set(expanded)
|
|
213
|
+
if (value) next.add(selected)
|
|
214
|
+
else next.delete(selected)
|
|
215
|
+
return next
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function addFileTreeNode(nodes: FileTreeNode[], roots: number[], input: Omit<FileTreeNode, "id" | "children">) {
|
|
219
|
+
const id = nodes.length
|
|
220
|
+
nodes.push({ ...input, id, children: [] })
|
|
221
|
+
if (input.parent === undefined) roots.push(id)
|
|
222
|
+
else nodes[input.parent]!.children.push(id)
|
|
223
|
+
return id
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function fileTreeParentDirectories(tree: FileTree, id: number) {
|
|
227
|
+
const result = new Set<number>()
|
|
228
|
+
for (let parent = tree.nodes[id]?.parent; parent !== undefined; parent = tree.nodes[parent]?.parent) {
|
|
229
|
+
result.add(parent)
|
|
230
|
+
}
|
|
231
|
+
return result
|
|
232
|
+
}
|