@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,80 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { onMount } from "solid-js"
|
|
3
|
+
import { createStore } from "solid-js/store"
|
|
4
|
+
import { createSimpleContext } from "../context/helper"
|
|
5
|
+
import { useTuiPaths } from "../context/runtime"
|
|
6
|
+
import { appendText, readText, writeText } from "../util/persistence"
|
|
7
|
+
|
|
8
|
+
type FrecencyEntry = { path: string; frequency: number; lastOpen: number }
|
|
9
|
+
|
|
10
|
+
export const MAX_FRECENCY_ENTRIES = 1000
|
|
11
|
+
|
|
12
|
+
export function parseFrecency(text: string) {
|
|
13
|
+
const latest = text
|
|
14
|
+
.split("\n")
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map((line) => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(line) as FrecencyEntry
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.filter((line): line is FrecencyEntry => line !== undefined)
|
|
24
|
+
.reduce<Record<string, FrecencyEntry>>((result, entry) => {
|
|
25
|
+
result[entry.path] = entry
|
|
26
|
+
return result
|
|
27
|
+
}, {})
|
|
28
|
+
return Object.values(latest)
|
|
29
|
+
.sort((a, b) => b.lastOpen - a.lastOpen)
|
|
30
|
+
.slice(0, MAX_FRECENCY_ENTRIES)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function calculateFrecency(entry?: { frequency: number; lastOpen: number }) {
|
|
34
|
+
if (!entry) return 0
|
|
35
|
+
return entry.frequency / (1 + (Date.now() - entry.lastOpen) / 86400000)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const { use: useFrecency, provider: FrecencyProvider } = createSimpleContext({
|
|
39
|
+
name: "Frecency",
|
|
40
|
+
init: () => {
|
|
41
|
+
const paths = useTuiPaths()
|
|
42
|
+
const frecencyPath = path.join(paths.state, "frecency.jsonl")
|
|
43
|
+
onMount(async () => {
|
|
44
|
+
const lines = parseFrecency(await readText(frecencyPath).catch(() => ""))
|
|
45
|
+
setStore(
|
|
46
|
+
"data",
|
|
47
|
+
Object.fromEntries(
|
|
48
|
+
lines.map((entry) => [entry.path, { frequency: entry.frequency, lastOpen: entry.lastOpen }]),
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
if (lines.length > 0)
|
|
52
|
+
writeText(frecencyPath, lines.map((entry) => JSON.stringify(entry)).join("\n") + "\n").catch(() => {})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const [store, setStore] = createStore({ data: {} as Record<string, { frequency: number; lastOpen: number }> })
|
|
56
|
+
|
|
57
|
+
function updateFrecency(filePath: string) {
|
|
58
|
+
const absolutePath = path.resolve(paths.cwd, filePath)
|
|
59
|
+
const newEntry = { frequency: (store.data[absolutePath]?.frequency || 0) + 1, lastOpen: Date.now() }
|
|
60
|
+
setStore("data", absolutePath, newEntry)
|
|
61
|
+
appendText(frecencyPath, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {})
|
|
62
|
+
|
|
63
|
+
if (Object.keys(store.data).length <= MAX_FRECENCY_ENTRIES) return
|
|
64
|
+
const sorted = Object.entries(store.data)
|
|
65
|
+
.sort(([, a], [, b]) => b.lastOpen - a.lastOpen)
|
|
66
|
+
.slice(0, MAX_FRECENCY_ENTRIES)
|
|
67
|
+
setStore("data", Object.fromEntries(sorted))
|
|
68
|
+
writeText(
|
|
69
|
+
frecencyPath,
|
|
70
|
+
sorted.map(([entryPath, entry]) => JSON.stringify({ path: entryPath, ...entry })).join("\n") + "\n",
|
|
71
|
+
).catch(() => {})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
getFrecency: (filePath: string) => calculateFrecency(store.data[path.resolve(paths.cwd, filePath)]),
|
|
76
|
+
updateFrecency,
|
|
77
|
+
data: () => store.data,
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { onMount } from "solid-js"
|
|
3
|
+
import { createStore, produce, unwrap } from "solid-js/store"
|
|
4
|
+
import type { AgentPart, FilePart, TextPart } from "@neurocode-ai/sdk/v2"
|
|
5
|
+
import { createSimpleContext } from "../context/helper"
|
|
6
|
+
import { useTuiPaths } from "../context/runtime"
|
|
7
|
+
import { appendText, readText, writeText } from "../util/persistence"
|
|
8
|
+
|
|
9
|
+
export type PromptInfo = {
|
|
10
|
+
input: string
|
|
11
|
+
mode?: "normal" | "shell"
|
|
12
|
+
parts: (
|
|
13
|
+
| Omit<FilePart, "id" | "messageID" | "sessionID">
|
|
14
|
+
| Omit<AgentPart, "id" | "messageID" | "sessionID">
|
|
15
|
+
| (Omit<TextPart, "id" | "messageID" | "sessionID"> & {
|
|
16
|
+
source?: {
|
|
17
|
+
text: {
|
|
18
|
+
start: number
|
|
19
|
+
end: number
|
|
20
|
+
value: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
)[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const MAX_HISTORY_ENTRIES = 50
|
|
28
|
+
|
|
29
|
+
export function parsePromptHistory(text: string) {
|
|
30
|
+
return text
|
|
31
|
+
.split("\n")
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((line) => {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(line) as PromptInfo
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.filter((line): line is PromptInfo => line !== undefined)
|
|
41
|
+
.slice(-MAX_HISTORY_ENTRIES)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isDuplicateEntry(previous: PromptInfo | undefined, next: PromptInfo): boolean {
|
|
45
|
+
if (!previous) return false
|
|
46
|
+
return JSON.stringify(previous) === JSON.stringify(next)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
|
|
50
|
+
name: "PromptHistory",
|
|
51
|
+
init: () => {
|
|
52
|
+
const paths = useTuiPaths()
|
|
53
|
+
const historyPath = path.join(paths.state, "prompt-history.jsonl")
|
|
54
|
+
onMount(async () => {
|
|
55
|
+
const lines = parsePromptHistory(await readText(historyPath).catch(() => ""))
|
|
56
|
+
setStore("history", lines)
|
|
57
|
+
|
|
58
|
+
// Rewrite valid retained entries to self-heal corruption and enforce the limit.
|
|
59
|
+
if (lines.length > 0)
|
|
60
|
+
writeText(historyPath, lines.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const [store, setStore] = createStore({
|
|
64
|
+
index: 0,
|
|
65
|
+
history: [] as PromptInfo[],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
move(direction: 1 | -1, input: string) {
|
|
70
|
+
if (!store.history.length) return undefined
|
|
71
|
+
const current = store.history.at(store.index)
|
|
72
|
+
if (!current) return undefined
|
|
73
|
+
if (current.input !== input && input.length) return
|
|
74
|
+
setStore(
|
|
75
|
+
produce((draft) => {
|
|
76
|
+
const next = store.index + direction
|
|
77
|
+
if (Math.abs(next) > store.history.length) return
|
|
78
|
+
if (next > 0) return
|
|
79
|
+
draft.index = next
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
if (store.index === 0) return { input: "", parts: [] }
|
|
83
|
+
return store.history.at(store.index)
|
|
84
|
+
},
|
|
85
|
+
append(item: PromptInfo) {
|
|
86
|
+
const entry = structuredClone(unwrap(item))
|
|
87
|
+
if (isDuplicateEntry(store.history.at(-1), entry)) {
|
|
88
|
+
setStore("index", 0)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
let trimmed = false
|
|
92
|
+
setStore(
|
|
93
|
+
produce((draft) => {
|
|
94
|
+
draft.history.push(entry)
|
|
95
|
+
if (draft.history.length > MAX_HISTORY_ENTRIES) {
|
|
96
|
+
draft.history = draft.history.slice(-MAX_HISTORY_ENTRIES)
|
|
97
|
+
trimmed = true
|
|
98
|
+
}
|
|
99
|
+
draft.index = 0
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (trimmed) {
|
|
104
|
+
writeText(historyPath, store.history.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
appendText(historyPath, JSON.stringify(entry) + "\n").catch(() => {})
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { displaySlice } from "./display"
|
|
2
|
+
|
|
3
|
+
export function stripPromptPartIDs<Part extends { id: string; messageID: string; sessionID: string }>(part: Part) {
|
|
4
|
+
const { id: _id, messageID: _messageID, sessionID: _sessionID, ...rest } = part
|
|
5
|
+
return rest
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function expandPastedTextPlaceholders(text: string, parts: readonly unknown[]) {
|
|
9
|
+
return parts.reduce<string>((result, part) => {
|
|
10
|
+
if (!isPastedTextPart(part)) return result
|
|
11
|
+
return result.replace(part.source.text.value, part.text)
|
|
12
|
+
}, text)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isPastedTextPart(part: unknown): part is { type: "text"; text: string; source: { text: { value: string } } } {
|
|
16
|
+
if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text") return false
|
|
17
|
+
if (!("text" in part) || typeof part.text !== "string" || !("source" in part)) return false
|
|
18
|
+
const source = part.source
|
|
19
|
+
if (!source || typeof source !== "object" || !("text" in source)) return false
|
|
20
|
+
const text = source.text
|
|
21
|
+
return Boolean(text && typeof text === "object" && "value" in text && typeof text.value === "string")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function expandTrackedPastedText(text: string, ranges: { start: number; end: number; text: string }[]) {
|
|
25
|
+
return ranges
|
|
26
|
+
.slice()
|
|
27
|
+
.sort((a, b) => b.start - a.start)
|
|
28
|
+
.reduce((result, part) => displaySlice(result, 0, part.start) + part.text + displaySlice(result, part.end), text)
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { onMount } from "solid-js"
|
|
3
|
+
import { createStore, produce, unwrap } from "solid-js/store"
|
|
4
|
+
import { createSimpleContext } from "../context/helper"
|
|
5
|
+
import { useTuiPaths } from "../context/runtime"
|
|
6
|
+
import { appendText, readText, writeText } from "../util/persistence"
|
|
7
|
+
import type { PromptInfo } from "./history"
|
|
8
|
+
|
|
9
|
+
export type StashEntry = {
|
|
10
|
+
input: string
|
|
11
|
+
parts: PromptInfo["parts"]
|
|
12
|
+
timestamp: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const MAX_STASH_ENTRIES = 50
|
|
16
|
+
|
|
17
|
+
export function parsePromptStash(text: string) {
|
|
18
|
+
return text
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((line) => {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(line) as StashEntry
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.filter((line): line is StashEntry => line !== undefined)
|
|
29
|
+
.slice(-MAX_STASH_ENTRIES)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const { use: usePromptStash, provider: PromptStashProvider } = createSimpleContext({
|
|
33
|
+
name: "PromptStash",
|
|
34
|
+
init: () => {
|
|
35
|
+
const paths = useTuiPaths()
|
|
36
|
+
const stashPath = path.join(paths.state, "prompt-stash.jsonl")
|
|
37
|
+
onMount(async () => {
|
|
38
|
+
const lines = parsePromptStash(await readText(stashPath).catch(() => ""))
|
|
39
|
+
setStore("entries", lines)
|
|
40
|
+
if (lines.length > 0)
|
|
41
|
+
writeText(stashPath, lines.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const [store, setStore] = createStore({ entries: [] as StashEntry[] })
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
list() {
|
|
48
|
+
return store.entries
|
|
49
|
+
},
|
|
50
|
+
push(entry: Omit<StashEntry, "timestamp">) {
|
|
51
|
+
const stash = structuredClone(unwrap({ ...entry, timestamp: Date.now() }))
|
|
52
|
+
let trimmed = false
|
|
53
|
+
setStore(
|
|
54
|
+
produce((draft) => {
|
|
55
|
+
draft.entries.push(stash)
|
|
56
|
+
if (draft.entries.length > MAX_STASH_ENTRIES) {
|
|
57
|
+
draft.entries = draft.entries.slice(-MAX_STASH_ENTRIES)
|
|
58
|
+
trimmed = true
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (trimmed) {
|
|
64
|
+
writeText(stashPath, store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n").catch(() => {})
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
appendText(stashPath, JSON.stringify(stash) + "\n").catch(() => {})
|
|
68
|
+
},
|
|
69
|
+
pop() {
|
|
70
|
+
if (store.entries.length === 0) return undefined
|
|
71
|
+
const entry = store.entries[store.entries.length - 1]
|
|
72
|
+
setStore(produce((draft) => void draft.entries.pop()))
|
|
73
|
+
writeText(
|
|
74
|
+
stashPath,
|
|
75
|
+
store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "",
|
|
76
|
+
).catch(() => {})
|
|
77
|
+
return entry
|
|
78
|
+
},
|
|
79
|
+
remove(index: number) {
|
|
80
|
+
if (index < 0 || index >= store.entries.length) return
|
|
81
|
+
setStore(produce((draft) => void draft.entries.splice(index, 1)))
|
|
82
|
+
writeText(
|
|
83
|
+
stashPath,
|
|
84
|
+
store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "",
|
|
85
|
+
).catch(() => {})
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EditorTraits } from "@opentui/core"
|
|
2
|
+
|
|
3
|
+
export type PromptMode = "normal" | "shell"
|
|
4
|
+
|
|
5
|
+
export interface PromptTraitsInput {
|
|
6
|
+
mode: PromptMode
|
|
7
|
+
autocompleteVisible: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PromptTraits = EditorTraits & {
|
|
11
|
+
owner: "neurocode"
|
|
12
|
+
role: "prompt"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The managed textarea keymap owns `suspend`; these traits only describe capture and status. */
|
|
16
|
+
export function computePromptTraits(input: PromptTraitsInput): PromptTraits {
|
|
17
|
+
const capture =
|
|
18
|
+
input.mode === "normal"
|
|
19
|
+
? input.autocompleteVisible
|
|
20
|
+
? (["escape", "navigate", "submit", "tab"] as const)
|
|
21
|
+
: (["tab"] as const)
|
|
22
|
+
: undefined
|
|
23
|
+
return {
|
|
24
|
+
capture,
|
|
25
|
+
status: input.mode === "shell" ? "SHELL" : undefined,
|
|
26
|
+
owner: "neurocode",
|
|
27
|
+
role: "prompt",
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createMemo,
|
|
4
|
+
createSignal,
|
|
5
|
+
useContext,
|
|
6
|
+
type Accessor,
|
|
7
|
+
type ParentProps,
|
|
8
|
+
type Setter,
|
|
9
|
+
} from "solid-js"
|
|
10
|
+
import { useSync } from "../../context/sync"
|
|
11
|
+
import { useTuiPaths } from "../../context/runtime"
|
|
12
|
+
|
|
13
|
+
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" }
|
|
14
|
+
|
|
15
|
+
type Context = {
|
|
16
|
+
destination: Accessor<HomeSessionDestination | undefined>
|
|
17
|
+
setDestination: Setter<HomeSessionDestination | undefined>
|
|
18
|
+
clear: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const HomeSessionDestinationContext = createContext<Context>()
|
|
22
|
+
|
|
23
|
+
export function HomeSessionDestinationProvider(props: ParentProps) {
|
|
24
|
+
const sync = useSync()
|
|
25
|
+
const paths = useTuiPaths()
|
|
26
|
+
const [selected, setDestination] = createSignal<HomeSessionDestination>()
|
|
27
|
+
const destination = createMemo<HomeSessionDestination>(
|
|
28
|
+
() => selected() ?? { type: "directory", directory: sync.path.directory || paths.cwd, subdirectory: false },
|
|
29
|
+
)
|
|
30
|
+
return (
|
|
31
|
+
<HomeSessionDestinationContext.Provider
|
|
32
|
+
value={{ destination, setDestination, clear: () => setDestination(undefined) }}
|
|
33
|
+
>
|
|
34
|
+
{props.children}
|
|
35
|
+
</HomeSessionDestinationContext.Provider>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useHomeSessionDestination() {
|
|
40
|
+
return useContext(HomeSessionDestinationContext)
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Prompt, type PromptRef } from "../component/prompt"
|
|
2
|
+
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
|
3
|
+
import { Logo } from "../component/logo"
|
|
4
|
+
import { useSync } from "../context/sync"
|
|
5
|
+
import { Toast } from "../ui/toast"
|
|
6
|
+
import { useArgs } from "../context/args"
|
|
7
|
+
import { useRouteData } from "../context/route"
|
|
8
|
+
import { usePromptRef } from "../context/prompt"
|
|
9
|
+
import { useLocal } from "../context/local"
|
|
10
|
+
import { usePluginRuntime } from "../plugin/runtime"
|
|
11
|
+
import { useEditorContext } from "../context/editor"
|
|
12
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
13
|
+
import { useTuiConfig } from "../config"
|
|
14
|
+
import { HomeSessionDestinationProvider } from "./home/session-destination"
|
|
15
|
+
|
|
16
|
+
let once = false
|
|
17
|
+
const placeholder = {
|
|
18
|
+
normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
|
|
19
|
+
shell: ["ls -la", "git status", "pwd"],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Home() {
|
|
23
|
+
const pluginRuntime = usePluginRuntime()
|
|
24
|
+
const sync = useSync()
|
|
25
|
+
const route = useRouteData("home")
|
|
26
|
+
const promptRef = usePromptRef()
|
|
27
|
+
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
|
28
|
+
const args = useArgs()
|
|
29
|
+
const local = useLocal()
|
|
30
|
+
const editor = useEditorContext()
|
|
31
|
+
const dimensions = useTerminalDimensions()
|
|
32
|
+
const tuiConfig = useTuiConfig()
|
|
33
|
+
const promptMaxWidth = createMemo(() => {
|
|
34
|
+
const configured = tuiConfig.prompt?.max_width
|
|
35
|
+
if (configured === "auto") return Math.max(75, Math.floor(dimensions().width * 0.7))
|
|
36
|
+
return configured ?? 75
|
|
37
|
+
})
|
|
38
|
+
let sent = false
|
|
39
|
+
|
|
40
|
+
onMount(() => {
|
|
41
|
+
editor.clearSelection()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const bind = (r: PromptRef | undefined) => {
|
|
45
|
+
setRef(r)
|
|
46
|
+
promptRef.set(r)
|
|
47
|
+
if (once || !r) return
|
|
48
|
+
if (route.prompt) {
|
|
49
|
+
r.set(route.prompt)
|
|
50
|
+
once = true
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (!args.prompt) return
|
|
54
|
+
r.set({ input: args.prompt, parts: [] })
|
|
55
|
+
once = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wait for sync and model store to be ready before auto-submitting --prompt
|
|
59
|
+
createEffect(() => {
|
|
60
|
+
const r = ref()
|
|
61
|
+
if (sent) return
|
|
62
|
+
if (!r) return
|
|
63
|
+
if (!sync.ready || !local.model.ready) return
|
|
64
|
+
if (!args.prompt) return
|
|
65
|
+
if (r.current.input !== args.prompt) return
|
|
66
|
+
sent = true
|
|
67
|
+
r.submit()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<HomeSessionDestinationProvider>
|
|
72
|
+
<box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
|
|
73
|
+
<box flexGrow={1} minHeight={0} />
|
|
74
|
+
<box height={4} minHeight={0} flexShrink={1} />
|
|
75
|
+
<box flexShrink={0}>
|
|
76
|
+
<pluginRuntime.Slot name="home_logo" mode="replace">
|
|
77
|
+
<Logo />
|
|
78
|
+
</pluginRuntime.Slot>
|
|
79
|
+
</box>
|
|
80
|
+
<box height={1} minHeight={0} flexShrink={1} />
|
|
81
|
+
<box width="100%" maxWidth={promptMaxWidth()} zIndex={1000} paddingTop={1} flexShrink={0}>
|
|
82
|
+
<pluginRuntime.Slot name="home_prompt" mode="replace" ref={bind}>
|
|
83
|
+
<Prompt ref={bind} right={<pluginRuntime.Slot name="home_prompt_right" />} placeholders={placeholder} />
|
|
84
|
+
</pluginRuntime.Slot>
|
|
85
|
+
</box>
|
|
86
|
+
<pluginRuntime.Slot name="home_bottom" />
|
|
87
|
+
<box flexGrow={1} minHeight={0} />
|
|
88
|
+
<Toast />
|
|
89
|
+
</box>
|
|
90
|
+
<box width="100%" flexShrink={0}>
|
|
91
|
+
<pluginRuntime.Slot name="home_footer" mode="single_winner" />
|
|
92
|
+
</box>
|
|
93
|
+
</HomeSessionDestinationProvider>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createMemo, onMount } from "solid-js"
|
|
2
|
+
import { useSync } from "../../context/sync"
|
|
3
|
+
import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select"
|
|
4
|
+
import type { TextPart } from "@neurocode-ai/sdk/v2"
|
|
5
|
+
import { Locale } from "../../util/locale"
|
|
6
|
+
import { useSDK } from "../../context/sdk"
|
|
7
|
+
import { useRoute } from "../../context/route"
|
|
8
|
+
import { useDialog, type DialogContext } from "../../ui/dialog"
|
|
9
|
+
import type { PromptInfo } from "../../component/prompt/history"
|
|
10
|
+
import { stripPromptPartIDs as strip } from "../../prompt/part"
|
|
11
|
+
|
|
12
|
+
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) {
|
|
13
|
+
const sync = useSync()
|
|
14
|
+
const dialog = useDialog()
|
|
15
|
+
const sdk = useSDK()
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
|
|
18
|
+
onMount(() => {
|
|
19
|
+
dialog.setSize("large")
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const options = createMemo((): DialogSelectOption<string | undefined>[] => {
|
|
23
|
+
const messages = sync.data.message[props.sessionID] ?? []
|
|
24
|
+
const fullSession = {
|
|
25
|
+
title: "Full session",
|
|
26
|
+
value: undefined,
|
|
27
|
+
onSelect: async (dialog: DialogContext) => {
|
|
28
|
+
const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
|
|
29
|
+
route.navigate({
|
|
30
|
+
sessionID: forked.data!.id,
|
|
31
|
+
type: "session",
|
|
32
|
+
})
|
|
33
|
+
dialog.clear()
|
|
34
|
+
},
|
|
35
|
+
} satisfies DialogSelectOption<string | undefined>
|
|
36
|
+
const result = [] as DialogSelectOption<string | undefined>[]
|
|
37
|
+
for (const message of messages) {
|
|
38
|
+
if (message.role !== "user") continue
|
|
39
|
+
const part = (sync.data.part[message.id] ?? []).find(
|
|
40
|
+
(x) => x.type === "text" && !x.synthetic && !x.ignored,
|
|
41
|
+
) as TextPart
|
|
42
|
+
if (!part) continue
|
|
43
|
+
result.push({
|
|
44
|
+
title: part.text.replace(/\n/g, " "),
|
|
45
|
+
value: message.id,
|
|
46
|
+
footer: Locale.time(message.time.created),
|
|
47
|
+
onSelect: async (dialog) => {
|
|
48
|
+
const forked = await sdk.client.session.fork({
|
|
49
|
+
sessionID: props.sessionID,
|
|
50
|
+
messageID: message.id,
|
|
51
|
+
})
|
|
52
|
+
const parts = sync.data.part[message.id] ?? []
|
|
53
|
+
const prompt = parts.reduce(
|
|
54
|
+
(agg, part) => {
|
|
55
|
+
if (part.type === "text") {
|
|
56
|
+
if (!part.synthetic) agg.input += part.text
|
|
57
|
+
}
|
|
58
|
+
if (part.type === "file") agg.parts.push(strip(part))
|
|
59
|
+
return agg
|
|
60
|
+
},
|
|
61
|
+
{ input: "", parts: [] as PromptInfo["parts"] },
|
|
62
|
+
)
|
|
63
|
+
route.navigate({
|
|
64
|
+
sessionID: forked.data!.id,
|
|
65
|
+
type: "session",
|
|
66
|
+
prompt,
|
|
67
|
+
})
|
|
68
|
+
dialog.clear()
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return [fullSession, ...result.reverse()]
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork session" options={options()} />
|
|
76
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createMemo } from "solid-js"
|
|
2
|
+
import { useSync } from "../../context/sync"
|
|
3
|
+
import { DialogSelect } from "../../ui/dialog-select"
|
|
4
|
+
import { useSDK } from "../../context/sdk"
|
|
5
|
+
import { useRoute } from "../../context/route"
|
|
6
|
+
import { useClipboard } from "../../context/clipboard"
|
|
7
|
+
import type { PromptInfo } from "../../component/prompt/history"
|
|
8
|
+
import { stripPromptPartIDs as strip } from "../../prompt/part"
|
|
9
|
+
|
|
10
|
+
export function DialogMessage(props: {
|
|
11
|
+
messageID: string
|
|
12
|
+
sessionID: string
|
|
13
|
+
setPrompt?: (prompt: PromptInfo) => void
|
|
14
|
+
}) {
|
|
15
|
+
const sync = useSync()
|
|
16
|
+
const sdk = useSDK()
|
|
17
|
+
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
|
|
18
|
+
const route = useRoute()
|
|
19
|
+
const clipboard = useClipboard()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<DialogSelect
|
|
23
|
+
title="Message Actions"
|
|
24
|
+
options={[
|
|
25
|
+
{
|
|
26
|
+
title: "Revert",
|
|
27
|
+
value: "session.revert",
|
|
28
|
+
description: "undo messages and file changes",
|
|
29
|
+
onSelect: (dialog) => {
|
|
30
|
+
const msg = message()
|
|
31
|
+
if (!msg) return
|
|
32
|
+
|
|
33
|
+
void sdk.client.session.revert({
|
|
34
|
+
sessionID: props.sessionID,
|
|
35
|
+
messageID: msg.id,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (props.setPrompt) {
|
|
39
|
+
const parts = sync.data.part[msg.id]
|
|
40
|
+
const promptInfo = parts.reduce(
|
|
41
|
+
(agg, part) => {
|
|
42
|
+
if (part.type === "text") {
|
|
43
|
+
if (!part.synthetic) agg.input += part.text
|
|
44
|
+
}
|
|
45
|
+
if (part.type === "file") agg.parts.push(strip(part))
|
|
46
|
+
return agg
|
|
47
|
+
},
|
|
48
|
+
{ input: "", parts: [] as PromptInfo["parts"] },
|
|
49
|
+
)
|
|
50
|
+
props.setPrompt(promptInfo)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
dialog.clear()
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: "Copy",
|
|
58
|
+
value: "message.copy",
|
|
59
|
+
description: "message text to clipboard",
|
|
60
|
+
onSelect: async (dialog) => {
|
|
61
|
+
const msg = message()
|
|
62
|
+
if (!msg) return
|
|
63
|
+
|
|
64
|
+
const parts = sync.data.part[msg.id]
|
|
65
|
+
const text = parts.reduce((agg, part) => {
|
|
66
|
+
if (part.type === "text" && !part.synthetic) {
|
|
67
|
+
agg += part.text
|
|
68
|
+
}
|
|
69
|
+
return agg
|
|
70
|
+
}, "")
|
|
71
|
+
|
|
72
|
+
await clipboard.write?.(text)
|
|
73
|
+
dialog.clear()
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
title: "Fork",
|
|
78
|
+
value: "session.fork",
|
|
79
|
+
description: "create a new session",
|
|
80
|
+
onSelect: async (dialog) => {
|
|
81
|
+
const result = await sdk.client.session.fork({
|
|
82
|
+
sessionID: props.sessionID,
|
|
83
|
+
messageID: props.messageID,
|
|
84
|
+
})
|
|
85
|
+
const msg = message()
|
|
86
|
+
const prompt = msg
|
|
87
|
+
? sync.data.part[msg.id].reduce(
|
|
88
|
+
(agg, part) => {
|
|
89
|
+
if (part.type === "text") {
|
|
90
|
+
if (!part.synthetic) agg.input += part.text
|
|
91
|
+
}
|
|
92
|
+
if (part.type === "file") agg.parts.push(part)
|
|
93
|
+
return agg
|
|
94
|
+
},
|
|
95
|
+
{ input: "", parts: [] as PromptInfo["parts"] },
|
|
96
|
+
)
|
|
97
|
+
: undefined
|
|
98
|
+
route.navigate({
|
|
99
|
+
sessionID: result.data!.id,
|
|
100
|
+
type: "session",
|
|
101
|
+
prompt,
|
|
102
|
+
})
|
|
103
|
+
dialog.clear()
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
]}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DialogSelect } from "../../ui/dialog-select"
|
|
2
|
+
import { useRoute } from "../../context/route"
|
|
3
|
+
|
|
4
|
+
export function DialogSubagent(props: { sessionID: string }) {
|
|
5
|
+
const route = useRoute()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<DialogSelect
|
|
9
|
+
title="Subagent Actions"
|
|
10
|
+
options={[
|
|
11
|
+
{
|
|
12
|
+
title: "Open",
|
|
13
|
+
value: "subagent.view",
|
|
14
|
+
description: "the subagent's session",
|
|
15
|
+
onSelect: (dialog) => {
|
|
16
|
+
route.navigate({
|
|
17
|
+
type: "session",
|
|
18
|
+
sessionID: props.sessionID,
|
|
19
|
+
})
|
|
20
|
+
dialog.clear()
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
]}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|