@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
package/src/editor.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core"
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
3
|
+
import { readFile, rm, writeFile } from "node:fs/promises"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { spawn } from "node:child_process"
|
|
7
|
+
import type { Stream } from "node:stream"
|
|
8
|
+
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
|
|
9
|
+
|
|
10
|
+
type EditorStdio = "inherit" | "pipe" | "ignore" | number | Stream
|
|
11
|
+
|
|
12
|
+
export function normalizePromptContent(content: string) {
|
|
13
|
+
if (content.endsWith("\r\n")) {
|
|
14
|
+
const body = content.slice(0, -2)
|
|
15
|
+
return !body.includes("\n") && !body.includes("\r") ? body : content
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (content.endsWith("\n")) {
|
|
19
|
+
const body = content.slice(0, -1)
|
|
20
|
+
return !body.includes("\n") && !body.includes("\r") ? body : content
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return content
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function openEditor(input: { value: string; renderer: CliRenderer; cwd?: string; stdin?: EditorStdio }) {
|
|
27
|
+
const editor = process.env.VISUAL || process.env.EDITOR
|
|
28
|
+
if (!editor) return
|
|
29
|
+
const file = path.join(os.tmpdir(), `${Date.now()}.md`)
|
|
30
|
+
await writeFile(file, input.value)
|
|
31
|
+
input.renderer.suspend()
|
|
32
|
+
input.renderer.currentRenderBuffer.clear()
|
|
33
|
+
try {
|
|
34
|
+
await new Promise<void>((resolve, reject) => {
|
|
35
|
+
const parts = editor.split(" ")
|
|
36
|
+
const child = spawn(parts[0]!, [...parts.slice(1), file], {
|
|
37
|
+
cwd: input.cwd && existsSync(input.cwd) ? input.cwd : process.cwd(),
|
|
38
|
+
stdio: [input.stdin ?? "inherit", "inherit", "inherit"],
|
|
39
|
+
shell: process.platform === "win32",
|
|
40
|
+
})
|
|
41
|
+
child.on("error", reject)
|
|
42
|
+
child.on("exit", (code, signal) => {
|
|
43
|
+
if (code === 0) return resolve()
|
|
44
|
+
reject(new Error(`Editor exited with ${signal ? `signal ${signal}` : `code ${code}`}`))
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
return (await readFile(file, "utf8")) || undefined
|
|
48
|
+
} finally {
|
|
49
|
+
await rm(file, { force: true }).catch(() => {})
|
|
50
|
+
input.renderer.currentRenderBuffer.clear()
|
|
51
|
+
input.renderer.resume()
|
|
52
|
+
input.renderer.requestRender()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function discoverEditorConnection(directory: string) {
|
|
57
|
+
const root = path.join(os.homedir(), ".claude", "ide")
|
|
58
|
+
const contains = (parent: string) => {
|
|
59
|
+
const resolved = path.resolve(parent)
|
|
60
|
+
const relative = path.relative(resolved, path.resolve(directory))
|
|
61
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved.length : 0
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return readdirSync(root)
|
|
65
|
+
.filter((entry) => entry.endsWith(".lock"))
|
|
66
|
+
.flatMap((entry) => {
|
|
67
|
+
const file = path.join(root, entry)
|
|
68
|
+
const port = Number.parseInt(path.basename(file, ".lock"), 10)
|
|
69
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) return []
|
|
70
|
+
try {
|
|
71
|
+
const value = JSON.parse(readFileSync(file, "utf8")) as Record<string, unknown>
|
|
72
|
+
if (value.transport !== undefined && value.transport !== "ws") return []
|
|
73
|
+
const folders = Array.isArray(value.workspaceFolders)
|
|
74
|
+
? value.workspaceFolders.filter((item): item is string => typeof item === "string")
|
|
75
|
+
: []
|
|
76
|
+
const score = Math.max(0, ...folders.map(contains))
|
|
77
|
+
if (!score) return []
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
url: `ws://127.0.0.1:${port}`,
|
|
81
|
+
authToken: typeof value.authToken === "string" ? value.authToken : undefined,
|
|
82
|
+
source: `lock:${port}`,
|
|
83
|
+
score,
|
|
84
|
+
mtime: statSync(file).mtimeMs,
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
} catch {
|
|
88
|
+
return []
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.sort((left, right) => right.score - left.score || right.mtime - left.mtime)
|
|
92
|
+
.map(({ url, authToken, source }) => ({ url, authToken, source }))[0]
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const editorIntegration = {
|
|
99
|
+
connection: discoverEditorConnection,
|
|
100
|
+
selection: (directory: string) => resolveZedSelection(resolveZedDbPath() ?? "", directory),
|
|
101
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginModule } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import HomeFooter from "./home/footer"
|
|
3
|
+
import HomeTips from "./home/tips"
|
|
4
|
+
import SidebarContext from "./sidebar/context"
|
|
5
|
+
import SidebarFiles from "./sidebar/files"
|
|
6
|
+
import SidebarFooter from "./sidebar/footer"
|
|
7
|
+
import SidebarLsp from "./sidebar/lsp"
|
|
8
|
+
import SidebarMcp from "./sidebar/mcp"
|
|
9
|
+
import SidebarTodo from "./sidebar/todo"
|
|
10
|
+
import DiffViewer from "./system/diff-viewer"
|
|
11
|
+
import Notifications from "./system/notifications"
|
|
12
|
+
import PluginManager from "./system/plugins"
|
|
13
|
+
import WhichKey from "./system/which-key"
|
|
14
|
+
|
|
15
|
+
export type BuiltinTuiPlugin = Omit<TuiPluginModule, "id"> & {
|
|
16
|
+
id: string
|
|
17
|
+
tui: TuiPlugin
|
|
18
|
+
enabled?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createBuiltinPlugins(options: { experimentalEventSystem: boolean }): BuiltinTuiPlugin[] {
|
|
22
|
+
return [
|
|
23
|
+
HomeFooter,
|
|
24
|
+
HomeTips,
|
|
25
|
+
SidebarContext,
|
|
26
|
+
SidebarMcp,
|
|
27
|
+
SidebarLsp,
|
|
28
|
+
SidebarTodo,
|
|
29
|
+
SidebarFiles,
|
|
30
|
+
SidebarFooter,
|
|
31
|
+
Notifications,
|
|
32
|
+
PluginManager,
|
|
33
|
+
WhichKey,
|
|
34
|
+
DiffViewer,
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
3
|
+
import { createMemo, Match, Show, Switch } from "solid-js"
|
|
4
|
+
import { abbreviateHome } from "../../runtime"
|
|
5
|
+
import { useTuiPaths } from "../../context/runtime"
|
|
6
|
+
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
|
7
|
+
|
|
8
|
+
const id = "internal:home-footer"
|
|
9
|
+
|
|
10
|
+
function Directory(props: { api: TuiPluginApi }) {
|
|
11
|
+
const theme = () => props.api.theme.current
|
|
12
|
+
const destination = useHomeSessionDestination()
|
|
13
|
+
const paths = useTuiPaths()
|
|
14
|
+
const dir = createMemo(() => {
|
|
15
|
+
const selected = destination?.destination()
|
|
16
|
+
if (!selected || selected.type === "new") return
|
|
17
|
+
const out = abbreviateHome(selected.directory, paths.home)
|
|
18
|
+
const branch =
|
|
19
|
+
selected.directory === (props.api.state.path.directory || paths.cwd) ? props.api.state.vcs?.branch : undefined
|
|
20
|
+
if (branch) return out + ":" + branch
|
|
21
|
+
return out
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return <Show when={dir()}>{(value) => <text fg={theme().textMuted}>{value()}</text>}</Show>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Mcp(props: { api: TuiPluginApi }) {
|
|
28
|
+
const theme = () => props.api.theme.current
|
|
29
|
+
const list = createMemo(() => props.api.state.mcp())
|
|
30
|
+
const has = createMemo(() => list().length > 0)
|
|
31
|
+
const err = createMemo(() => list().some((item) => item.status === "failed"))
|
|
32
|
+
const count = createMemo(() => list().filter((item) => item.status === "connected").length)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Show when={has()}>
|
|
36
|
+
<box gap={1} flexDirection="row" flexShrink={0}>
|
|
37
|
+
<text fg={theme().text}>
|
|
38
|
+
<Switch>
|
|
39
|
+
<Match when={err()}>
|
|
40
|
+
<span style={{ fg: theme().error }}>⊙ </span>
|
|
41
|
+
</Match>
|
|
42
|
+
<Match when={true}>
|
|
43
|
+
<span style={{ fg: count() > 0 ? theme().success : theme().textMuted }}>⊙ </span>
|
|
44
|
+
</Match>
|
|
45
|
+
</Switch>
|
|
46
|
+
{count()} MCP
|
|
47
|
+
</text>
|
|
48
|
+
<text fg={theme().textMuted}>/status</text>
|
|
49
|
+
</box>
|
|
50
|
+
</Show>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Version(props: { api: TuiPluginApi }) {
|
|
55
|
+
const theme = () => props.api.theme.current
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<box flexShrink={0}>
|
|
59
|
+
<text fg={theme().textMuted}>{props.api.app.version}</text>
|
|
60
|
+
</box>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function View(props: { api: TuiPluginApi }) {
|
|
65
|
+
return (
|
|
66
|
+
<box
|
|
67
|
+
width="100%"
|
|
68
|
+
paddingTop={1}
|
|
69
|
+
paddingBottom={1}
|
|
70
|
+
paddingLeft={2}
|
|
71
|
+
paddingRight={2}
|
|
72
|
+
flexDirection="row"
|
|
73
|
+
flexShrink={0}
|
|
74
|
+
gap={2}
|
|
75
|
+
>
|
|
76
|
+
<Directory api={props.api} />
|
|
77
|
+
<Mcp api={props.api} />
|
|
78
|
+
<box flexGrow={1} />
|
|
79
|
+
<Version api={props.api} />
|
|
80
|
+
</box>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tui: TuiPlugin = async (api) => {
|
|
85
|
+
api.slots.register({
|
|
86
|
+
order: 100,
|
|
87
|
+
slots: {
|
|
88
|
+
home_footer() {
|
|
89
|
+
return <View api={api} />
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const plugin: BuiltinTuiPlugin = {
|
|
96
|
+
id,
|
|
97
|
+
tui,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default plugin
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
2
|
+
import { createMemo, For, type Accessor } from "solid-js"
|
|
3
|
+
import { DEFAULT_THEMES, useTheme } from "../../context/theme"
|
|
4
|
+
import { useCommandShortcut } from "../../keymap"
|
|
5
|
+
|
|
6
|
+
const themeCount = Object.keys(DEFAULT_THEMES).length
|
|
7
|
+
|
|
8
|
+
type TipPart = { text: string; highlight: boolean }
|
|
9
|
+
type TipShortcut = Accessor<string>
|
|
10
|
+
type Shortcuts = {
|
|
11
|
+
agentCycle: TipShortcut
|
|
12
|
+
childFirst: TipShortcut
|
|
13
|
+
childNext: TipShortcut
|
|
14
|
+
childPrevious: TipShortcut
|
|
15
|
+
commandList: TipShortcut
|
|
16
|
+
editorOpen: TipShortcut
|
|
17
|
+
helpShow: TipShortcut
|
|
18
|
+
inputClear: TipShortcut
|
|
19
|
+
inputNewline: TipShortcut
|
|
20
|
+
inputPaste: TipShortcut
|
|
21
|
+
inputUndo: TipShortcut
|
|
22
|
+
leader: TipShortcut
|
|
23
|
+
messagesCopy: TipShortcut
|
|
24
|
+
messagesFirst: TipShortcut
|
|
25
|
+
messagesLast: TipShortcut
|
|
26
|
+
messagesPageDown: TipShortcut
|
|
27
|
+
messagesPageUp: TipShortcut
|
|
28
|
+
messagesToggleConceal: TipShortcut
|
|
29
|
+
modelCycleRecent: TipShortcut
|
|
30
|
+
modelList: TipShortcut
|
|
31
|
+
sessionExport: TipShortcut
|
|
32
|
+
sessionInterrupt: TipShortcut
|
|
33
|
+
sessionList: TipShortcut
|
|
34
|
+
sessionNew: TipShortcut
|
|
35
|
+
sessionParent: TipShortcut
|
|
36
|
+
sessionPinToggle: TipShortcut
|
|
37
|
+
sessionQuickSwitch1: TipShortcut
|
|
38
|
+
sessionQuickSwitch9: TipShortcut
|
|
39
|
+
sessionSidebarToggle: TipShortcut
|
|
40
|
+
sessionTimeline: TipShortcut
|
|
41
|
+
statusView: TipShortcut
|
|
42
|
+
terminalSuspend: TipShortcut
|
|
43
|
+
themeList: TipShortcut
|
|
44
|
+
}
|
|
45
|
+
type Tip = string | ((shortcuts: Shortcuts) => string | undefined)
|
|
46
|
+
|
|
47
|
+
function parse(tip: string): TipPart[] {
|
|
48
|
+
const parts: TipPart[] = []
|
|
49
|
+
const regex = /\{highlight\}(.*?)\{\/highlight\}/g
|
|
50
|
+
const found = Array.from(tip.matchAll(regex))
|
|
51
|
+
const state = found.reduce(
|
|
52
|
+
(acc, match) => {
|
|
53
|
+
const start = match.index ?? 0
|
|
54
|
+
if (start > acc.index) {
|
|
55
|
+
acc.parts.push({ text: tip.slice(acc.index, start), highlight: false })
|
|
56
|
+
}
|
|
57
|
+
acc.parts.push({ text: match[1], highlight: true })
|
|
58
|
+
acc.index = start + match[0].length
|
|
59
|
+
return acc
|
|
60
|
+
},
|
|
61
|
+
{ parts, index: 0 },
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (state.index < tip.length) {
|
|
65
|
+
parts.push({ text: tip.slice(state.index), highlight: false })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parts
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const NO_MODELS_TIP = "Run {highlight}/connect{/highlight} to add an AI provider and start coding"
|
|
72
|
+
const NO_MODELS_PARTS = parse(NO_MODELS_TIP)
|
|
73
|
+
|
|
74
|
+
function shortcutText(value: string) {
|
|
75
|
+
return `{highlight}${value}{/highlight}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function commandText(command: string, shortcut: string) {
|
|
79
|
+
if (!shortcut) return shortcutText(command)
|
|
80
|
+
return `${shortcutText(command)} or ${shortcutText(shortcut)}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function press(shortcut: string, text: string) {
|
|
84
|
+
if (!shortcut) return undefined
|
|
85
|
+
return `Press ${shortcutText(shortcut)} ${text}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function configShortcut(api: TuiPluginApi, command: string): TipShortcut {
|
|
89
|
+
return () =>
|
|
90
|
+
api.tuiConfig.keybinds
|
|
91
|
+
.get(command)
|
|
92
|
+
.map((binding) => api.keys.formatSequence(Array.from(api.keymap.parseKeySequence(binding.key))))
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(", ")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
|
|
98
|
+
const theme = useTheme().theme
|
|
99
|
+
const tipOffset = Math.random()
|
|
100
|
+
const shortcuts: Shortcuts = {
|
|
101
|
+
agentCycle: useCommandShortcut("agent.cycle"),
|
|
102
|
+
childFirst: configShortcut(props.api, "session.child.first"),
|
|
103
|
+
childNext: configShortcut(props.api, "session.child.next"),
|
|
104
|
+
childPrevious: configShortcut(props.api, "session.child.previous"),
|
|
105
|
+
commandList: useCommandShortcut("command.palette.show"),
|
|
106
|
+
editorOpen: useCommandShortcut("prompt.editor"),
|
|
107
|
+
helpShow: useCommandShortcut("help.show"),
|
|
108
|
+
inputClear: useCommandShortcut("prompt.clear"),
|
|
109
|
+
inputNewline: useCommandShortcut("input.newline"),
|
|
110
|
+
inputPaste: useCommandShortcut("prompt.paste"),
|
|
111
|
+
inputUndo: useCommandShortcut("input.undo"),
|
|
112
|
+
leader: configShortcut(props.api, "leader"),
|
|
113
|
+
messagesCopy: configShortcut(props.api, "messages.copy"),
|
|
114
|
+
messagesFirst: configShortcut(props.api, "session.first"),
|
|
115
|
+
messagesLast: configShortcut(props.api, "session.last"),
|
|
116
|
+
messagesPageDown: configShortcut(props.api, "session.page.down"),
|
|
117
|
+
messagesPageUp: configShortcut(props.api, "session.page.up"),
|
|
118
|
+
messagesToggleConceal: configShortcut(props.api, "session.toggle.conceal"),
|
|
119
|
+
modelCycleRecent: useCommandShortcut("model.cycle_recent"),
|
|
120
|
+
modelList: useCommandShortcut("model.list"),
|
|
121
|
+
sessionExport: configShortcut(props.api, "session.export"),
|
|
122
|
+
sessionInterrupt: configShortcut(props.api, "session.interrupt"),
|
|
123
|
+
sessionList: useCommandShortcut("session.list"),
|
|
124
|
+
sessionNew: useCommandShortcut("session.new"),
|
|
125
|
+
sessionParent: configShortcut(props.api, "session.parent"),
|
|
126
|
+
sessionPinToggle: configShortcut(props.api, "session.pin.toggle"),
|
|
127
|
+
sessionQuickSwitch1: useCommandShortcut("session.quick_switch.1"),
|
|
128
|
+
sessionQuickSwitch9: useCommandShortcut("session.quick_switch.9"),
|
|
129
|
+
sessionSidebarToggle: configShortcut(props.api, "session.sidebar.toggle"),
|
|
130
|
+
sessionTimeline: configShortcut(props.api, "session.timeline"),
|
|
131
|
+
statusView: useCommandShortcut("opencode.status"),
|
|
132
|
+
terminalSuspend: useCommandShortcut("terminal.suspend"),
|
|
133
|
+
themeList: useCommandShortcut("theme.switch"),
|
|
134
|
+
}
|
|
135
|
+
const tip = createMemo(() => {
|
|
136
|
+
if (props.connected === false) return NO_MODELS_TIP
|
|
137
|
+
const tips = [...TIPS, process.platform !== "win32" ? TERMINAL_SUSPEND_TIP : INPUT_UNDO_TIP].flatMap((item) => {
|
|
138
|
+
const value = typeof item === "string" ? item : item(shortcuts)
|
|
139
|
+
return value ? [value] : []
|
|
140
|
+
})
|
|
141
|
+
return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP
|
|
142
|
+
}, NO_MODELS_TIP)
|
|
143
|
+
// Solid can expose a memo's initial value while a pure computation is pending.
|
|
144
|
+
const parts = createMemo(() => {
|
|
145
|
+
const value = tip()
|
|
146
|
+
if (typeof value === "string") return parse(value)
|
|
147
|
+
return NO_MODELS_PARTS
|
|
148
|
+
}, NO_MODELS_PARTS)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<box flexDirection="row" maxWidth="100%">
|
|
152
|
+
<text flexShrink={0} style={{ fg: theme.warning }}>
|
|
153
|
+
● Tip{" "}
|
|
154
|
+
</text>
|
|
155
|
+
<text flexShrink={1} wrapMode="word">
|
|
156
|
+
<For each={parts()}>
|
|
157
|
+
{(part) => <span style={{ fg: part.highlight ? theme.text : theme.textMuted }}>{part.text}</span>}
|
|
158
|
+
</For>
|
|
159
|
+
</text>
|
|
160
|
+
</box>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const TIPS: Tip[] = [
|
|
165
|
+
"Type {highlight}@{/highlight} followed by a filename to fuzzy search and attach files",
|
|
166
|
+
"Start a message with {highlight}!{/highlight} to run shell commands (e.g., {highlight}!ls -la{/highlight})",
|
|
167
|
+
(shortcuts) => press(shortcuts.agentCycle(), "to cycle between Build and Plan agents"),
|
|
168
|
+
"Use {highlight}/undo{/highlight} to revert the last message and file changes",
|
|
169
|
+
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes",
|
|
170
|
+
"Run {highlight}/share{/highlight} to create a public opencode.ai link",
|
|
171
|
+
"Drag and drop images or PDFs into the terminal as context",
|
|
172
|
+
(shortcuts) => press(shortcuts.inputPaste(), "to paste images from your clipboard into the prompt"),
|
|
173
|
+
(shortcuts) => `Use ${commandText("/editor", shortcuts.editorOpen())} to compose messages in your external editor`,
|
|
174
|
+
"Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase",
|
|
175
|
+
(shortcuts) => `Use ${commandText("/models", shortcuts.modelList())} to switch between available AI models`,
|
|
176
|
+
(shortcuts) => `Use ${commandText("/themes", shortcuts.themeList())} to switch between ${themeCount} built-in themes`,
|
|
177
|
+
(shortcuts) => `Use ${commandText("/new", shortcuts.sessionNew())} to start a fresh conversation session`,
|
|
178
|
+
(shortcuts) => `Use ${commandText("/sessions", shortcuts.sessionList())} to list, pin, and continue sessions`,
|
|
179
|
+
(shortcuts) => press(shortcuts.sessionPinToggle(), "in the session list to pin one at the top"),
|
|
180
|
+
(shortcuts) =>
|
|
181
|
+
shortcuts.sessionQuickSwitch1() && shortcuts.sessionQuickSwitch9()
|
|
182
|
+
? `Use ${shortcutText(shortcuts.sessionQuickSwitch1())} through ${shortcutText(shortcuts.sessionQuickSwitch9())} to switch pinned sessions`
|
|
183
|
+
: undefined,
|
|
184
|
+
"Run {highlight}/compact{/highlight} to summarize long sessions near context limits",
|
|
185
|
+
(shortcuts) => `Use ${commandText("/export", shortcuts.sessionExport())} to save the conversation as Markdown`,
|
|
186
|
+
(shortcuts) => press(shortcuts.messagesCopy(), "to copy the assistant's last message to clipboard"),
|
|
187
|
+
(shortcuts) => press(shortcuts.commandList(), "to see all available actions and commands"),
|
|
188
|
+
"Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers",
|
|
189
|
+
(shortcuts) => `The leader key is ${shortcutText(shortcuts.leader())}; combine with other keys for quick actions`,
|
|
190
|
+
(shortcuts) => press(shortcuts.modelCycleRecent(), "to quickly switch between recently used models"),
|
|
191
|
+
(shortcuts) => press(shortcuts.sessionSidebarToggle(), "in a session to show or hide the sidebar panel"),
|
|
192
|
+
(shortcuts) =>
|
|
193
|
+
shortcuts.messagesPageUp() && shortcuts.messagesPageDown()
|
|
194
|
+
? `Use ${shortcutText(shortcuts.messagesPageUp())}/${shortcutText(shortcuts.messagesPageDown())} to navigate through conversation history`
|
|
195
|
+
: undefined,
|
|
196
|
+
(shortcuts) => press(shortcuts.messagesFirst(), "to jump to the beginning of the conversation"),
|
|
197
|
+
(shortcuts) => press(shortcuts.messagesLast(), "to jump to the most recent message"),
|
|
198
|
+
(shortcuts) => press(shortcuts.inputNewline(), "to add newlines in your prompt"),
|
|
199
|
+
(shortcuts) => press(shortcuts.inputClear(), "when typing to clear the input field"),
|
|
200
|
+
(shortcuts) => press(shortcuts.sessionInterrupt(), "to stop the AI mid-response"),
|
|
201
|
+
"Switch to {highlight}Plan{/highlight} agent for suggestions without making changes",
|
|
202
|
+
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents",
|
|
203
|
+
(shortcuts) => {
|
|
204
|
+
const items = [
|
|
205
|
+
shortcuts.sessionParent(),
|
|
206
|
+
shortcuts.childFirst(),
|
|
207
|
+
shortcuts.childPrevious(),
|
|
208
|
+
shortcuts.childNext(),
|
|
209
|
+
].filter(Boolean)
|
|
210
|
+
if (!items.length) return undefined
|
|
211
|
+
return `Use ${items.map(shortcutText).join(" / ")} for parent/child sessions`
|
|
212
|
+
},
|
|
213
|
+
"Create {highlight}opencode.json{/highlight} for server settings, and {highlight}tui.json{/highlight} for TUI",
|
|
214
|
+
"Place TUI settings in {highlight}~/.config/opencode/tui.json{/highlight} for global config",
|
|
215
|
+
"Add {highlight}$schema{/highlight} to your config for autocomplete in your editor",
|
|
216
|
+
"Configure {highlight}model{/highlight} in config to set your default model",
|
|
217
|
+
"Override any keybind in {highlight}tui.json{/highlight} via the {highlight}keybinds{/highlight} section",
|
|
218
|
+
"Set any keybind to {highlight}none{/highlight} to disable it completely",
|
|
219
|
+
"Configure local or remote MCP servers in the {highlight}mcp{/highlight} config section",
|
|
220
|
+
"Add {highlight}.md{/highlight} files to {highlight}.opencode/commands/{/highlight} for reusable prompts",
|
|
221
|
+
"Use {highlight}$ARGUMENTS{/highlight}, {highlight}$1{/highlight}, {highlight}$2{/highlight} in custom commands for dynamic input",
|
|
222
|
+
"Use backticks to inject shell output (e.g., {highlight}`git status`{/highlight})",
|
|
223
|
+
"Add {highlight}.md{/highlight} files to {highlight}.opencode/agents/{/highlight} for specialized AI personas",
|
|
224
|
+
"Configure per-agent permissions for {highlight}edit{/highlight}, {highlight}bash{/highlight}, and {highlight}webfetch{/highlight} tools",
|
|
225
|
+
'Use patterns like {highlight}"git *": "allow"{/highlight} for granular bash permissions',
|
|
226
|
+
'Set {highlight}"rm -rf *": "deny"{/highlight} to block destructive commands',
|
|
227
|
+
'Configure {highlight}"git push": "ask"{/highlight} to require approval before pushing',
|
|
228
|
+
'Set {highlight}"formatter": true{/highlight} to enable built-in formatters',
|
|
229
|
+
'Set {highlight}"formatter": false{/highlight} to disable inherited formatters',
|
|
230
|
+
"Define custom formatter commands with file extensions in config",
|
|
231
|
+
'Set {highlight}"lsp": true{/highlight} to enable built-in LSP code analysis',
|
|
232
|
+
"Create {highlight}.ts{/highlight} files in {highlight}.opencode/tools/{/highlight} to define new LLM tools",
|
|
233
|
+
"Tool definitions can invoke scripts written in Python, Go, etc",
|
|
234
|
+
"Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugins/{/highlight} for event hooks",
|
|
235
|
+
"Use plugins to send OS notifications when sessions complete",
|
|
236
|
+
"Create a plugin to prevent NeuroCode from reading sensitive files",
|
|
237
|
+
"Use {highlight}opencode run{/highlight} for non-interactive scripting",
|
|
238
|
+
"Use {highlight}opencode --continue{/highlight} to resume the last session",
|
|
239
|
+
"Use {highlight}opencode run -f file.ts{/highlight} to attach files via CLI",
|
|
240
|
+
"Use {highlight}--format json{/highlight} for machine-readable output in scripts",
|
|
241
|
+
"Run {highlight}neurocode serve{/highlight} for headless API access to NeuroCode",
|
|
242
|
+
"Use {highlight}opencode run --attach{/highlight} to connect to a running server",
|
|
243
|
+
"Run {highlight}opencode upgrade{/highlight} to update to the latest version",
|
|
244
|
+
"Run {highlight}opencode auth list{/highlight} to see all configured providers",
|
|
245
|
+
"Run {highlight}opencode agent create{/highlight} for guided agent creation",
|
|
246
|
+
"Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions",
|
|
247
|
+
"Run {highlight}opencode github install{/highlight} to set up the GitHub workflow",
|
|
248
|
+
"Comment {highlight}/opencode fix this{/highlight} on issues to auto-create PRs",
|
|
249
|
+
"Comment {highlight}/oc{/highlight} on PR code lines for targeted code reviews",
|
|
250
|
+
'Use {highlight}"theme": "system"{/highlight} to match your terminal\'s colors',
|
|
251
|
+
"Create JSON theme files in {highlight}.opencode/themes/{/highlight} directory",
|
|
252
|
+
"Themes support dark/light variants for both modes",
|
|
253
|
+
"Use numeric xterm color codes 0-255 in custom theme JSON",
|
|
254
|
+
"Use {highlight}{env:VAR_NAME}{/highlight} for environment variables in config",
|
|
255
|
+
"Use {highlight}{file:path}{/highlight} to include file contents in config values",
|
|
256
|
+
"Use {highlight}instructions{/highlight} in config to load additional rules files",
|
|
257
|
+
"Set agent {highlight}temperature{/highlight} from 0.0 (focused) to 1.0 (creative)",
|
|
258
|
+
"Configure {highlight}steps{/highlight} to limit agentic iterations per request",
|
|
259
|
+
'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools',
|
|
260
|
+
'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server',
|
|
261
|
+
"Override global tool settings per agent configuration",
|
|
262
|
+
'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions',
|
|
263
|
+
'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing',
|
|
264
|
+
"Run {highlight}/unshare{/highlight} to remove a session from public access",
|
|
265
|
+
"Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops",
|
|
266
|
+
"Permission {highlight}external_directory{/highlight} protects files outside project",
|
|
267
|
+
"Run {highlight}opencode debug config{/highlight} to troubleshoot configuration",
|
|
268
|
+
"Use {highlight}--print-logs{/highlight} flag to see detailed logs in stderr",
|
|
269
|
+
(shortcuts) => `Use ${commandText("/timeline", shortcuts.sessionTimeline())} to jump to specific messages`,
|
|
270
|
+
(shortcuts) => press(shortcuts.messagesToggleConceal(), "to toggle code block visibility in messages"),
|
|
271
|
+
(shortcuts) => `Use ${commandText("/status", shortcuts.statusView())} to see system status info`,
|
|
272
|
+
"Enable {highlight}scroll_acceleration{/highlight} in {highlight}tui.json{/highlight} for smooth scrolling",
|
|
273
|
+
(shortcuts) =>
|
|
274
|
+
shortcuts.commandList()
|
|
275
|
+
? `Toggle username display in chat via the command palette (${shortcutText(shortcuts.commandList())})`
|
|
276
|
+
: "Toggle username display in chat via the command palette",
|
|
277
|
+
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} in a container",
|
|
278
|
+
"Use {highlight}/connect{/highlight} with NeuroCode Zen for curated, tested models",
|
|
279
|
+
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing",
|
|
280
|
+
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
|
|
281
|
+
(shortcuts) => `Use ${commandText("/help", shortcuts.helpShow())} to show the help dialog`,
|
|
282
|
+
"Use {highlight}/rename{/highlight} to rename the current session",
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
const INPUT_UNDO_TIP: Tip = (shortcuts) => press(shortcuts.inputUndo(), "to undo changes in your prompt")
|
|
286
|
+
const TERMINAL_SUSPEND_TIP: Tip = (shortcuts) =>
|
|
287
|
+
press(shortcuts.terminalSuspend(), "to suspend the terminal and return to your shell")
|
|
@@ -0,0 +1,59 @@
|
|
|
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 { Tips } from "./tips-view"
|
|
5
|
+
import { useBindings } from "../../keymap"
|
|
6
|
+
|
|
7
|
+
const id = "internal:home-tips"
|
|
8
|
+
|
|
9
|
+
function View(props: { api: TuiPluginApi; hidden: boolean; show: boolean; connected: boolean }) {
|
|
10
|
+
useBindings(() => ({
|
|
11
|
+
commands: [
|
|
12
|
+
{
|
|
13
|
+
name: "tips.toggle",
|
|
14
|
+
title: props.hidden ? "Show tips" : "Hide tips",
|
|
15
|
+
category: "System",
|
|
16
|
+
namespace: "palette",
|
|
17
|
+
run() {
|
|
18
|
+
props.api.kv.set("tips_hidden", !props.api.kv.get("tips_hidden", false))
|
|
19
|
+
props.api.ui.dialog.clear()
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
bindings: props.api.tuiConfig.keybinds.get("tips.toggle"),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<box width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>
|
|
28
|
+
<Show when={props.show}>
|
|
29
|
+
<Tips api={props.api} connected={props.connected} />
|
|
30
|
+
</Show>
|
|
31
|
+
</box>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tui: TuiPlugin = async (api) => {
|
|
36
|
+
api.slots.register({
|
|
37
|
+
order: 100,
|
|
38
|
+
slots: {
|
|
39
|
+
home_bottom() {
|
|
40
|
+
const hidden = createMemo(() => api.kv.get("tips_hidden", false))
|
|
41
|
+
const first = createMemo(() => api.state.session.count() === 0)
|
|
42
|
+
const connected = createMemo(() =>
|
|
43
|
+
api.state.provider.some(
|
|
44
|
+
(item) => item.id !== "opencode" || Object.values(item.models).some((model) => model.cost?.input !== 0),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
const show = createMemo(() => (!first() || !connected()) && !hidden())
|
|
48
|
+
return <View api={api} hidden={hidden()} show={show()} connected={connected()} />
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const plugin: BuiltinTuiPlugin = {
|
|
55
|
+
id,
|
|
56
|
+
tui,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default plugin
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@neurocode-ai/sdk/v2"
|
|
2
|
+
import type { TuiPlugin, TuiPluginApi } from "@neurocode-ai/plugin/tui"
|
|
3
|
+
import type { BuiltinTuiPlugin } from "../builtins"
|
|
4
|
+
import { createMemo } from "solid-js"
|
|
5
|
+
|
|
6
|
+
const id = "internal:sidebar-context"
|
|
7
|
+
|
|
8
|
+
const money = new Intl.NumberFormat("en-US", {
|
|
9
|
+
style: "currency",
|
|
10
|
+
currency: "USD",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
14
|
+
const theme = () => props.api.theme.current
|
|
15
|
+
const msg = createMemo(() => props.api.state.session.messages(props.session_id))
|
|
16
|
+
const session = createMemo(() => props.api.state.session.get(props.session_id))
|
|
17
|
+
const cost = createMemo(() => session()?.cost ?? 0)
|
|
18
|
+
|
|
19
|
+
const state = createMemo(() => {
|
|
20
|
+
const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
|
21
|
+
if (!last) {
|
|
22
|
+
return {
|
|
23
|
+
tokens: 0,
|
|
24
|
+
percent: null,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tokens =
|
|
29
|
+
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
30
|
+
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
31
|
+
return {
|
|
32
|
+
tokens,
|
|
33
|
+
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null,
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<box>
|
|
39
|
+
<text fg={theme().text}>
|
|
40
|
+
<b>Context</b>
|
|
41
|
+
</text>
|
|
42
|
+
<text fg={theme().textMuted}>{state().tokens.toLocaleString()} tokens</text>
|
|
43
|
+
<text fg={theme().textMuted}>{state().percent ?? 0}% used</text>
|
|
44
|
+
<text fg={theme().textMuted}>{money.format(cost())} spent</text>
|
|
45
|
+
</box>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tui: TuiPlugin = async (api) => {
|
|
50
|
+
api.slots.register({
|
|
51
|
+
order: 100,
|
|
52
|
+
slots: {
|
|
53
|
+
sidebar_content(_ctx, props) {
|
|
54
|
+
return <View api={api} session_id={props.session_id} />
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const plugin: BuiltinTuiPlugin = {
|
|
61
|
+
id,
|
|
62
|
+
tui,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default plugin
|