@agentprojectcontext/apx 1.15.5 → 1.16.0
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/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
2
|
+
import type { InternalTuiPlugin } from "../../plugin/internal"
|
|
3
|
+
import { useSyncV2 } from "@tui/context/sync-v2"
|
|
4
|
+
import { SplitBorder } from "@tui/component/border"
|
|
5
|
+
import { Spinner } from "@tui/component/spinner"
|
|
6
|
+
import { useTheme } from "@tui/context/theme"
|
|
7
|
+
import { useLocal } from "@tui/context/local"
|
|
8
|
+
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
|
9
|
+
import { TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core"
|
|
10
|
+
import { useBindings } from "../../keymap"
|
|
11
|
+
import { Locale } from "@/util/locale"
|
|
12
|
+
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
|
13
|
+
import { webSearchProviderLabel } from "@/tool/websearch"
|
|
14
|
+
import path from "path"
|
|
15
|
+
import stripAnsi from "strip-ansi"
|
|
16
|
+
import type {
|
|
17
|
+
SessionMessage,
|
|
18
|
+
SessionMessageAgentSwitched,
|
|
19
|
+
SessionMessageAssistant,
|
|
20
|
+
SessionMessageAssistantReasoning,
|
|
21
|
+
SessionMessageAssistantText,
|
|
22
|
+
SessionMessageAssistantTool,
|
|
23
|
+
SessionMessageCompaction,
|
|
24
|
+
SessionMessageModelSwitched,
|
|
25
|
+
SessionMessageShell,
|
|
26
|
+
SessionMessageSynthetic,
|
|
27
|
+
SessionMessageUser,
|
|
28
|
+
ToolFileContent,
|
|
29
|
+
ToolTextContent,
|
|
30
|
+
} from "@opencode-ai/sdk/v2"
|
|
31
|
+
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
|
32
|
+
|
|
33
|
+
const id = "internal:session-v2-debug"
|
|
34
|
+
const route = "session.v2.messages"
|
|
35
|
+
|
|
36
|
+
function currentSessionID(api: TuiPluginApi) {
|
|
37
|
+
const current = api.route.current
|
|
38
|
+
if (current.name !== "session") return
|
|
39
|
+
const sessionID = current.params?.sessionID
|
|
40
|
+
return typeof sessionID === "string" ? sessionID : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function View(props: { api: TuiPluginApi; sessionID: string }) {
|
|
44
|
+
const sync = useSyncV2()
|
|
45
|
+
const dimensions = useTerminalDimensions()
|
|
46
|
+
const { theme, syntax, subtleSyntax } = useTheme()
|
|
47
|
+
const messages = createMemo(() => sync.data.messages[props.sessionID] ?? [])
|
|
48
|
+
const renderedMessages = createMemo(() => messages().toReversed())
|
|
49
|
+
const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant"))
|
|
50
|
+
const lastUserCreated = (index: number) =>
|
|
51
|
+
renderedMessages()
|
|
52
|
+
.slice(0, index)
|
|
53
|
+
.findLast((message) => message.type === "user")?.time.created
|
|
54
|
+
|
|
55
|
+
createEffect(() => {
|
|
56
|
+
void sync.session.message.sync(props.sessionID)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
useBindings(() => ({
|
|
60
|
+
bindings: [
|
|
61
|
+
{
|
|
62
|
+
key: "escape",
|
|
63
|
+
desc: "Back to session",
|
|
64
|
+
group: "Session",
|
|
65
|
+
cmd() {
|
|
66
|
+
props.api.route.navigate("session", { sessionID: props.sessionID })
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background}>
|
|
74
|
+
<box flexDirection="row">
|
|
75
|
+
<box flexGrow={1} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}>
|
|
76
|
+
<scrollbox
|
|
77
|
+
viewportOptions={{ paddingRight: 0 }}
|
|
78
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
79
|
+
stickyScroll={true}
|
|
80
|
+
stickyStart="bottom"
|
|
81
|
+
flexGrow={1}
|
|
82
|
+
>
|
|
83
|
+
<box height={1} />
|
|
84
|
+
<Show when={messages().length === 0}>
|
|
85
|
+
<MissingData label="Messages" detail="No v2 messages loaded from useSyncV2 yet." />
|
|
86
|
+
</Show>
|
|
87
|
+
<For each={renderedMessages()}>
|
|
88
|
+
{(message, index) => (
|
|
89
|
+
<Switch>
|
|
90
|
+
<Match when={message.type === "user"}>
|
|
91
|
+
<UserMessage message={message as SessionMessageUser} index={index()} />
|
|
92
|
+
</Match>
|
|
93
|
+
<Match when={message.type === "assistant"}>
|
|
94
|
+
<AssistantMessage
|
|
95
|
+
message={message as SessionMessageAssistant}
|
|
96
|
+
sessionID={props.sessionID}
|
|
97
|
+
last={lastAssistant()?.id === message.id}
|
|
98
|
+
syntax={syntax()}
|
|
99
|
+
subtleSyntax={subtleSyntax()}
|
|
100
|
+
start={lastUserCreated(index())}
|
|
101
|
+
/>
|
|
102
|
+
</Match>
|
|
103
|
+
<Match when={message.type === "synthetic"}>
|
|
104
|
+
<></>
|
|
105
|
+
</Match>
|
|
106
|
+
<Match when={message.type === "shell"}>
|
|
107
|
+
<ShellMessage message={message as SessionMessageShell} />
|
|
108
|
+
</Match>
|
|
109
|
+
<Match when={message.type === "compaction"}>
|
|
110
|
+
<CompactionMessage message={message as SessionMessageCompaction} />
|
|
111
|
+
</Match>
|
|
112
|
+
<Match when={message.type === "agent-switched"}>
|
|
113
|
+
<AgentSwitchedMessage message={message as SessionMessageAgentSwitched} />
|
|
114
|
+
</Match>
|
|
115
|
+
<Match when={message.type === "model-switched"}>
|
|
116
|
+
<ModelSwitchedMessage message={message as SessionMessageModelSwitched} />
|
|
117
|
+
</Match>
|
|
118
|
+
<Match when={true}>
|
|
119
|
+
<UnknownMessage message={message} />
|
|
120
|
+
</Match>
|
|
121
|
+
</Switch>
|
|
122
|
+
)}
|
|
123
|
+
</For>
|
|
124
|
+
</scrollbox>
|
|
125
|
+
<MissingData
|
|
126
|
+
label="Session prompt, permission prompt, question prompt, sidebar"
|
|
127
|
+
detail="The v2 message endpoint only exposes messages, so these session UI regions cannot be rendered here. Press Esc to return to the live session."
|
|
128
|
+
/>
|
|
129
|
+
</box>
|
|
130
|
+
</box>
|
|
131
|
+
</box>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function MissingData(props: { label: string; detail: string }) {
|
|
136
|
+
const { theme } = useTheme()
|
|
137
|
+
return (
|
|
138
|
+
<box
|
|
139
|
+
border={["left"]}
|
|
140
|
+
customBorderChars={SplitBorder.customBorderChars}
|
|
141
|
+
borderColor={theme.warning}
|
|
142
|
+
backgroundColor={theme.backgroundPanel}
|
|
143
|
+
paddingLeft={2}
|
|
144
|
+
paddingTop={1}
|
|
145
|
+
paddingBottom={1}
|
|
146
|
+
marginTop={1}
|
|
147
|
+
flexShrink={0}
|
|
148
|
+
>
|
|
149
|
+
<text fg={theme.text}>
|
|
150
|
+
<span style={{ bg: theme.warning, fg: theme.background, bold: true }}> MISSING DATA </span> {props.label}
|
|
151
|
+
</text>
|
|
152
|
+
<text fg={theme.textMuted}>{props.detail}</text>
|
|
153
|
+
</box>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function UserMessage(props: { message: SessionMessageUser; index: number }) {
|
|
158
|
+
const { theme } = useTheme()
|
|
159
|
+
const attachments = createMemo(() => [...(props.message.files ?? []), ...(props.message.agents ?? [])])
|
|
160
|
+
return (
|
|
161
|
+
<box
|
|
162
|
+
id={props.message.id}
|
|
163
|
+
border={["left"]}
|
|
164
|
+
borderColor={theme.secondary}
|
|
165
|
+
customBorderChars={SplitBorder.customBorderChars}
|
|
166
|
+
marginTop={props.index === 0 ? 0 : 1}
|
|
167
|
+
flexShrink={0}
|
|
168
|
+
paddingTop={1}
|
|
169
|
+
paddingBottom={1}
|
|
170
|
+
paddingLeft={2}
|
|
171
|
+
backgroundColor={theme.backgroundPanel}
|
|
172
|
+
>
|
|
173
|
+
<text fg={theme.text}>{props.message.text}</text>
|
|
174
|
+
<Show when={attachments().length}>
|
|
175
|
+
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
|
176
|
+
<For each={props.message.files ?? []}>
|
|
177
|
+
{(file) => (
|
|
178
|
+
<text fg={theme.text}>
|
|
179
|
+
<span style={{ bg: theme.secondary, fg: theme.background }}> {file.mime} </span>
|
|
180
|
+
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.name ?? file.uri} </span>
|
|
181
|
+
</text>
|
|
182
|
+
)}
|
|
183
|
+
</For>
|
|
184
|
+
<For each={props.message.agents ?? []}>
|
|
185
|
+
{(agent) => (
|
|
186
|
+
<text fg={theme.text}>
|
|
187
|
+
<span style={{ bg: theme.accent, fg: theme.background }}> agent </span>
|
|
188
|
+
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {agent.name} </span>
|
|
189
|
+
</text>
|
|
190
|
+
)}
|
|
191
|
+
</For>
|
|
192
|
+
</box>
|
|
193
|
+
</Show>
|
|
194
|
+
</box>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function ShellMessage(props: { message: SessionMessageShell }) {
|
|
199
|
+
const { theme } = useTheme()
|
|
200
|
+
const output = createMemo(() => stripAnsi(props.message.output.trim()))
|
|
201
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
202
|
+
const lines = createMemo(() => output().split("\n"))
|
|
203
|
+
const overflow = createMemo(() => lines().length > 10)
|
|
204
|
+
const limited = createMemo(() => {
|
|
205
|
+
if (expanded() || !overflow()) return output()
|
|
206
|
+
return [...lines().slice(0, 10), "…"].join("\n")
|
|
207
|
+
})
|
|
208
|
+
return (
|
|
209
|
+
<BlockTool
|
|
210
|
+
title="# Shell"
|
|
211
|
+
spinner={!props.message.time.completed}
|
|
212
|
+
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
|
213
|
+
>
|
|
214
|
+
<box gap={1}>
|
|
215
|
+
<text fg={theme.text}>$ {props.message.command}</text>
|
|
216
|
+
<Show when={output()}>
|
|
217
|
+
<text fg={theme.text}>{limited()}</text>
|
|
218
|
+
</Show>
|
|
219
|
+
<Show when={overflow()}>
|
|
220
|
+
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
|
221
|
+
</Show>
|
|
222
|
+
</box>
|
|
223
|
+
</BlockTool>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function CompactionMessage(props: { message: SessionMessageCompaction }) {
|
|
228
|
+
const { theme, syntax } = useTheme()
|
|
229
|
+
return (
|
|
230
|
+
<box
|
|
231
|
+
marginTop={1}
|
|
232
|
+
border={["top"]}
|
|
233
|
+
title={props.message.reason === "auto" ? " Auto Compaction " : " Compaction "}
|
|
234
|
+
titleAlignment="center"
|
|
235
|
+
borderColor={theme.borderActive}
|
|
236
|
+
flexShrink={0}
|
|
237
|
+
>
|
|
238
|
+
<Show when={props.message.summary}>
|
|
239
|
+
{(summary) => (
|
|
240
|
+
<box paddingLeft={3} paddingTop={1}>
|
|
241
|
+
<code
|
|
242
|
+
filetype="markdown"
|
|
243
|
+
drawUnstyledText={false}
|
|
244
|
+
streaming={false}
|
|
245
|
+
syntaxStyle={syntax()}
|
|
246
|
+
content={summary().trim()}
|
|
247
|
+
conceal={true}
|
|
248
|
+
fg={theme.text}
|
|
249
|
+
/>
|
|
250
|
+
</box>
|
|
251
|
+
)}
|
|
252
|
+
</Show>
|
|
253
|
+
</box>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function AgentSwitchedMessage(props: { message: SessionMessageAgentSwitched }) {
|
|
258
|
+
const { theme } = useTheme()
|
|
259
|
+
const local = useLocal()
|
|
260
|
+
return (
|
|
261
|
+
<box paddingLeft={3} marginTop={1} flexShrink={0}>
|
|
262
|
+
<text>
|
|
263
|
+
<span style={{ fg: local.agent.color(props.message.agent) }}>▣ </span>
|
|
264
|
+
<span style={{ fg: theme.textMuted }}>Switched agent to </span>
|
|
265
|
+
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.agent)}</span>
|
|
266
|
+
</text>
|
|
267
|
+
</box>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function ModelSwitchedMessage(props: { message: SessionMessageModelSwitched }) {
|
|
272
|
+
const { theme } = useTheme()
|
|
273
|
+
const model = createMemo(() => {
|
|
274
|
+
const variant = props.message.model.variant ? `/${props.message.model.variant}` : ""
|
|
275
|
+
return `${props.message.model.providerID}/${props.message.model.id}${variant}`
|
|
276
|
+
})
|
|
277
|
+
return (
|
|
278
|
+
<box paddingLeft={3} marginTop={1} flexShrink={0}>
|
|
279
|
+
<text>
|
|
280
|
+
<span style={{ fg: theme.secondary }}>◇ </span>
|
|
281
|
+
<span style={{ fg: theme.textMuted }}>Switched model to </span>
|
|
282
|
+
<span style={{ fg: theme.text }}>{model()}</span>
|
|
283
|
+
</text>
|
|
284
|
+
</box>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function UnknownMessage(props: { message: SessionMessage }) {
|
|
289
|
+
return <MissingData label="Unknown message type" detail={JSON.stringify(props.message)} />
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function AssistantMessage(props: {
|
|
293
|
+
message: SessionMessageAssistant
|
|
294
|
+
sessionID: string
|
|
295
|
+
last: boolean
|
|
296
|
+
syntax: SyntaxStyle
|
|
297
|
+
subtleSyntax: SyntaxStyle
|
|
298
|
+
start?: number
|
|
299
|
+
}) {
|
|
300
|
+
const { theme } = useTheme()
|
|
301
|
+
const local = useLocal()
|
|
302
|
+
const duration = createMemo(() => {
|
|
303
|
+
if (!props.message.time.completed) return 0
|
|
304
|
+
return props.message.time.completed - (props.start ?? props.message.time.created)
|
|
305
|
+
})
|
|
306
|
+
const model = createMemo(() => {
|
|
307
|
+
const variant = props.message.model.variant ? `/${props.message.model.variant}` : ""
|
|
308
|
+
return `${props.message.model.providerID}/${props.message.model.id}${variant}`
|
|
309
|
+
})
|
|
310
|
+
const final = createMemo(() => props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish))
|
|
311
|
+
return (
|
|
312
|
+
<>
|
|
313
|
+
<For each={props.message.content}>
|
|
314
|
+
{(part) => (
|
|
315
|
+
<Switch>
|
|
316
|
+
<Match when={part.type === "text"}>
|
|
317
|
+
<AssistantText part={part as SessionMessageAssistantText} syntax={props.syntax} />
|
|
318
|
+
</Match>
|
|
319
|
+
<Match when={part.type === "reasoning"}>
|
|
320
|
+
<AssistantReasoning part={part as SessionMessageAssistantReasoning} subtleSyntax={props.subtleSyntax} />
|
|
321
|
+
</Match>
|
|
322
|
+
<Match when={part.type === "tool"}>
|
|
323
|
+
<AssistantTool part={part as SessionMessageAssistantTool} sessionID={props.sessionID} />
|
|
324
|
+
</Match>
|
|
325
|
+
</Switch>
|
|
326
|
+
)}
|
|
327
|
+
</For>
|
|
328
|
+
<Show when={props.message.content.length === 0}>
|
|
329
|
+
<MissingData label="Assistant content" detail={`Assistant message ${props.message.id} has no content items.`} />
|
|
330
|
+
</Show>
|
|
331
|
+
<Show when={props.message.error}>
|
|
332
|
+
<box
|
|
333
|
+
border={["left"]}
|
|
334
|
+
paddingTop={1}
|
|
335
|
+
paddingBottom={1}
|
|
336
|
+
paddingLeft={2}
|
|
337
|
+
marginTop={1}
|
|
338
|
+
backgroundColor={theme.backgroundPanel}
|
|
339
|
+
customBorderChars={SplitBorder.customBorderChars}
|
|
340
|
+
borderColor={theme.error}
|
|
341
|
+
flexShrink={0}
|
|
342
|
+
>
|
|
343
|
+
<text fg={theme.textMuted}>{props.message.error}</text>
|
|
344
|
+
</box>
|
|
345
|
+
</Show>
|
|
346
|
+
<Show when={props.last || final() || props.message.error}>
|
|
347
|
+
<box paddingLeft={3} flexShrink={0}>
|
|
348
|
+
<text marginTop={1}>
|
|
349
|
+
<span style={{ fg: local.agent.color(props.message.agent) }}>▣ </span>
|
|
350
|
+
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.agent)}</span>
|
|
351
|
+
<span style={{ fg: theme.textMuted }}> · {model()}</span>
|
|
352
|
+
<Show when={duration()}>
|
|
353
|
+
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
|
354
|
+
</Show>
|
|
355
|
+
</text>
|
|
356
|
+
</box>
|
|
357
|
+
</Show>
|
|
358
|
+
</>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function AssistantText(props: { part: SessionMessageAssistantText; syntax: SyntaxStyle }) {
|
|
363
|
+
const { theme } = useTheme()
|
|
364
|
+
return (
|
|
365
|
+
<Show when={props.part.text.trim()}>
|
|
366
|
+
<box paddingLeft={3} marginTop={1} flexShrink={0} id="text">
|
|
367
|
+
<code
|
|
368
|
+
filetype="markdown"
|
|
369
|
+
drawUnstyledText={false}
|
|
370
|
+
streaming={true}
|
|
371
|
+
syntaxStyle={props.syntax}
|
|
372
|
+
content={props.part.text.trim()}
|
|
373
|
+
conceal={true}
|
|
374
|
+
fg={theme.text}
|
|
375
|
+
/>
|
|
376
|
+
</box>
|
|
377
|
+
</Show>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function AssistantReasoning(props: { part: SessionMessageAssistantReasoning; subtleSyntax: SyntaxStyle }) {
|
|
382
|
+
const { theme } = useTheme()
|
|
383
|
+
const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim())
|
|
384
|
+
return (
|
|
385
|
+
<Show when={content()}>
|
|
386
|
+
<box
|
|
387
|
+
paddingLeft={2}
|
|
388
|
+
marginTop={1}
|
|
389
|
+
flexDirection="column"
|
|
390
|
+
border={["left"]}
|
|
391
|
+
customBorderChars={SplitBorder.customBorderChars}
|
|
392
|
+
borderColor={theme.backgroundElement}
|
|
393
|
+
flexShrink={0}
|
|
394
|
+
>
|
|
395
|
+
<code
|
|
396
|
+
filetype="markdown"
|
|
397
|
+
drawUnstyledText={false}
|
|
398
|
+
streaming={true}
|
|
399
|
+
syntaxStyle={props.subtleSyntax}
|
|
400
|
+
content={"_Thinking:_ " + content()}
|
|
401
|
+
conceal={true}
|
|
402
|
+
fg={theme.textMuted}
|
|
403
|
+
/>
|
|
404
|
+
</box>
|
|
405
|
+
</Show>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function AssistantTool(props: { part: SessionMessageAssistantTool; sessionID: string }) {
|
|
410
|
+
const input = createMemo(() => toolInputRecord(props.part.state.input))
|
|
411
|
+
const toolprops = {
|
|
412
|
+
get input() {
|
|
413
|
+
return input()
|
|
414
|
+
},
|
|
415
|
+
get metadata() {
|
|
416
|
+
return props.part.provider?.metadata ?? {}
|
|
417
|
+
},
|
|
418
|
+
get output() {
|
|
419
|
+
return props.part.state.status === "pending" ? undefined : toolOutput(props.part.state.content)
|
|
420
|
+
},
|
|
421
|
+
sessionID: props.sessionID,
|
|
422
|
+
part: props.part,
|
|
423
|
+
}
|
|
424
|
+
return (
|
|
425
|
+
<Switch>
|
|
426
|
+
<Match when={props.part.name === "bash"}>
|
|
427
|
+
<Bash {...toolprops} />
|
|
428
|
+
</Match>
|
|
429
|
+
<Match when={props.part.name === "glob"}>
|
|
430
|
+
<Glob {...toolprops} />
|
|
431
|
+
</Match>
|
|
432
|
+
<Match when={props.part.name === "read"}>
|
|
433
|
+
<Read {...toolprops} />
|
|
434
|
+
</Match>
|
|
435
|
+
<Match when={props.part.name === "grep"}>
|
|
436
|
+
<Grep {...toolprops} />
|
|
437
|
+
</Match>
|
|
438
|
+
<Match when={props.part.name === "webfetch"}>
|
|
439
|
+
<WebFetch {...toolprops} />
|
|
440
|
+
</Match>
|
|
441
|
+
<Match when={props.part.name === "websearch"}>
|
|
442
|
+
<WebSearch {...toolprops} />
|
|
443
|
+
</Match>
|
|
444
|
+
<Match when={props.part.name === "write"}>
|
|
445
|
+
<Write {...toolprops} />
|
|
446
|
+
</Match>
|
|
447
|
+
<Match when={props.part.name === "edit"}>
|
|
448
|
+
<Edit {...toolprops} />
|
|
449
|
+
</Match>
|
|
450
|
+
<Match when={props.part.name === "apply_patch"}>
|
|
451
|
+
<ApplyPatch {...toolprops} />
|
|
452
|
+
</Match>
|
|
453
|
+
<Match when={props.part.name === "todowrite"}>
|
|
454
|
+
<TodoWrite {...toolprops} />
|
|
455
|
+
</Match>
|
|
456
|
+
<Match when={props.part.name === "question"}>
|
|
457
|
+
<Question {...toolprops} />
|
|
458
|
+
</Match>
|
|
459
|
+
<Match when={props.part.name === "skill"}>
|
|
460
|
+
<Skill {...toolprops} />
|
|
461
|
+
</Match>
|
|
462
|
+
<Match when={props.part.name === "task"}>
|
|
463
|
+
<Task {...toolprops} />
|
|
464
|
+
</Match>
|
|
465
|
+
<Match when={true}>
|
|
466
|
+
<GenericTool {...toolprops} />
|
|
467
|
+
</Match>
|
|
468
|
+
</Switch>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
type ToolProps = {
|
|
473
|
+
input: Record<string, unknown>
|
|
474
|
+
metadata: Record<string, unknown>
|
|
475
|
+
output?: string
|
|
476
|
+
sessionID: string
|
|
477
|
+
part: SessionMessageAssistantTool
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function GenericTool(props: ToolProps) {
|
|
481
|
+
const { theme } = useTheme()
|
|
482
|
+
const output = createMemo(() => props.output?.trim() ?? "")
|
|
483
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
484
|
+
const lines = createMemo(() => output().split("\n"))
|
|
485
|
+
const maxLines = 3
|
|
486
|
+
const overflow = createMemo(() => lines().length > maxLines)
|
|
487
|
+
const limited = createMemo(() => {
|
|
488
|
+
if (expanded() || !overflow()) return output()
|
|
489
|
+
return [...lines().slice(0, maxLines), "…"].join("\n")
|
|
490
|
+
})
|
|
491
|
+
return (
|
|
492
|
+
<Show
|
|
493
|
+
when={output()}
|
|
494
|
+
fallback={
|
|
495
|
+
<InlineTool icon="⚙" pending="Writing command..." complete={toolComplete(props.part)} part={props.part}>
|
|
496
|
+
{props.part.name} {input(props.input)}
|
|
497
|
+
</InlineTool>
|
|
498
|
+
}
|
|
499
|
+
>
|
|
500
|
+
<BlockTool
|
|
501
|
+
title={`# ${props.part.name} ${input(props.input)}`}
|
|
502
|
+
part={props.part}
|
|
503
|
+
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
|
504
|
+
>
|
|
505
|
+
<box gap={1}>
|
|
506
|
+
<text fg={theme.text}>{limited()}</text>
|
|
507
|
+
<Show when={overflow()}>
|
|
508
|
+
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
|
509
|
+
</Show>
|
|
510
|
+
</box>
|
|
511
|
+
</BlockTool>
|
|
512
|
+
</Show>
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function InlineTool(props: {
|
|
517
|
+
icon: string
|
|
518
|
+
complete: unknown
|
|
519
|
+
pending: string
|
|
520
|
+
spinner?: boolean
|
|
521
|
+
children: JSX.Element
|
|
522
|
+
part: SessionMessageAssistantTool
|
|
523
|
+
}) {
|
|
524
|
+
const { theme } = useTheme()
|
|
525
|
+
const renderer = useRenderer()
|
|
526
|
+
const [margin, setMargin] = createSignal(0)
|
|
527
|
+
const [hover, setHover] = createSignal(false)
|
|
528
|
+
const [showError, setShowError] = createSignal(false)
|
|
529
|
+
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined))
|
|
530
|
+
const complete = createMemo(() => !!props.complete)
|
|
531
|
+
const denied = createMemo(() => {
|
|
532
|
+
const message = error()
|
|
533
|
+
if (!message) return false
|
|
534
|
+
return (
|
|
535
|
+
message.includes("QuestionRejectedError") ||
|
|
536
|
+
message.includes("rejected permission") ||
|
|
537
|
+
message.includes("specified a rule") ||
|
|
538
|
+
message.includes("user dismissed")
|
|
539
|
+
)
|
|
540
|
+
})
|
|
541
|
+
const fg = createMemo(() => {
|
|
542
|
+
if (error()) return theme.error
|
|
543
|
+
if (complete()) return theme.textMuted
|
|
544
|
+
return theme.text
|
|
545
|
+
})
|
|
546
|
+
const attributes = createMemo(() => (denied() ? TextAttributes.STRIKETHROUGH : undefined))
|
|
547
|
+
return (
|
|
548
|
+
<box
|
|
549
|
+
marginTop={margin()}
|
|
550
|
+
paddingLeft={3}
|
|
551
|
+
flexShrink={0}
|
|
552
|
+
flexDirection="row"
|
|
553
|
+
gap={1}
|
|
554
|
+
backgroundColor={hover() && error() ? theme.backgroundMenu : undefined}
|
|
555
|
+
onMouseOver={() => error() && setHover(true)}
|
|
556
|
+
onMouseOut={() => setHover(false)}
|
|
557
|
+
onMouseUp={() => {
|
|
558
|
+
if (!error()) return
|
|
559
|
+
if (renderer.getSelection()?.getSelectedText()) return
|
|
560
|
+
setShowError((prev) => !prev)
|
|
561
|
+
}}
|
|
562
|
+
renderBefore={function () {
|
|
563
|
+
const el = this as BoxRenderable
|
|
564
|
+
const parent = el.parent
|
|
565
|
+
if (!parent) return
|
|
566
|
+
const previous = parent.getChildren()[parent.getChildren().indexOf(el) - 1]
|
|
567
|
+
if (!previous) {
|
|
568
|
+
setMargin(0)
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
if (previous.id.startsWith("text")) setMargin(1)
|
|
572
|
+
}}
|
|
573
|
+
>
|
|
574
|
+
<box flexShrink={0}>
|
|
575
|
+
<Switch>
|
|
576
|
+
<Match when={props.spinner}>
|
|
577
|
+
<Spinner color={theme.text} />
|
|
578
|
+
</Match>
|
|
579
|
+
<Match when={complete()}>
|
|
580
|
+
<text fg={fg()} attributes={attributes()}>
|
|
581
|
+
{props.icon}
|
|
582
|
+
</text>
|
|
583
|
+
</Match>
|
|
584
|
+
<Match when={true}>
|
|
585
|
+
<text fg={fg()} attributes={attributes()}>
|
|
586
|
+
~
|
|
587
|
+
</text>
|
|
588
|
+
</Match>
|
|
589
|
+
</Switch>
|
|
590
|
+
</box>
|
|
591
|
+
<box flexGrow={1}>
|
|
592
|
+
<box>
|
|
593
|
+
<Switch>
|
|
594
|
+
<Match when={complete()}>
|
|
595
|
+
<text fg={fg()} attributes={attributes()}>
|
|
596
|
+
{props.children}
|
|
597
|
+
</text>
|
|
598
|
+
</Match>
|
|
599
|
+
<Match when={true}>
|
|
600
|
+
<text fg={fg()} attributes={attributes()}>
|
|
601
|
+
{props.pending}
|
|
602
|
+
</text>
|
|
603
|
+
</Match>
|
|
604
|
+
</Switch>
|
|
605
|
+
</box>
|
|
606
|
+
<Show when={showError() && error()}>
|
|
607
|
+
<box>
|
|
608
|
+
<text fg={theme.error}>{error()}</text>
|
|
609
|
+
</box>
|
|
610
|
+
</Show>
|
|
611
|
+
</box>
|
|
612
|
+
</box>
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function BlockTool(props: {
|
|
617
|
+
title: string
|
|
618
|
+
children: JSX.Element
|
|
619
|
+
part?: SessionMessageAssistantTool
|
|
620
|
+
onClick?: () => void
|
|
621
|
+
spinner?: boolean
|
|
622
|
+
}) {
|
|
623
|
+
const { theme } = useTheme()
|
|
624
|
+
const renderer = useRenderer()
|
|
625
|
+
const [hover, setHover] = createSignal(false)
|
|
626
|
+
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error.message : undefined))
|
|
627
|
+
return (
|
|
628
|
+
<box
|
|
629
|
+
border={["left"]}
|
|
630
|
+
paddingTop={1}
|
|
631
|
+
paddingBottom={1}
|
|
632
|
+
paddingLeft={2}
|
|
633
|
+
marginTop={1}
|
|
634
|
+
gap={1}
|
|
635
|
+
backgroundColor={hover() ? theme.backgroundMenu : theme.backgroundPanel}
|
|
636
|
+
customBorderChars={SplitBorder.customBorderChars}
|
|
637
|
+
borderColor={theme.background}
|
|
638
|
+
onMouseOver={() => props.onClick && setHover(true)}
|
|
639
|
+
onMouseOut={() => setHover(false)}
|
|
640
|
+
onMouseUp={() => {
|
|
641
|
+
if (renderer.getSelection()?.getSelectedText()) return
|
|
642
|
+
props.onClick?.()
|
|
643
|
+
}}
|
|
644
|
+
flexShrink={0}
|
|
645
|
+
>
|
|
646
|
+
<Show
|
|
647
|
+
when={props.spinner}
|
|
648
|
+
fallback={
|
|
649
|
+
<text paddingLeft={3} fg={theme.textMuted}>
|
|
650
|
+
{props.title}
|
|
651
|
+
</text>
|
|
652
|
+
}
|
|
653
|
+
>
|
|
654
|
+
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
|
|
655
|
+
</Show>
|
|
656
|
+
{props.children}
|
|
657
|
+
<Show when={error()}>
|
|
658
|
+
<text fg={theme.error}>{error()}</text>
|
|
659
|
+
</Show>
|
|
660
|
+
</box>
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function Bash(props: ToolProps) {
|
|
665
|
+
const { theme } = useTheme()
|
|
666
|
+
const output = createMemo(() => stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim()))
|
|
667
|
+
const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part))
|
|
668
|
+
const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`)
|
|
669
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
670
|
+
const lines = createMemo(() => output().split("\n"))
|
|
671
|
+
const overflow = createMemo(() => lines().length > 10)
|
|
672
|
+
const limited = createMemo(() => {
|
|
673
|
+
if (expanded() || !overflow()) return output()
|
|
674
|
+
return [...lines().slice(0, 10), "…"].join("\n")
|
|
675
|
+
})
|
|
676
|
+
return (
|
|
677
|
+
<Switch>
|
|
678
|
+
<Match when={output()}>
|
|
679
|
+
<BlockTool
|
|
680
|
+
title={title()}
|
|
681
|
+
part={props.part}
|
|
682
|
+
spinner={props.part.state.status === "running"}
|
|
683
|
+
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
|
684
|
+
>
|
|
685
|
+
<box gap={1}>
|
|
686
|
+
<text fg={theme.text}>$ {command()}</text>
|
|
687
|
+
<text fg={theme.text}>{limited()}</text>
|
|
688
|
+
<Show when={overflow()}>
|
|
689
|
+
<text fg={theme.textMuted}>{expanded() ? "Click to collapse" : "Click to expand"}</text>
|
|
690
|
+
</Show>
|
|
691
|
+
</box>
|
|
692
|
+
</BlockTool>
|
|
693
|
+
</Match>
|
|
694
|
+
<Match when={true}>
|
|
695
|
+
<InlineTool icon="$" pending="Writing command..." complete={command()} part={props.part}>
|
|
696
|
+
{command()}
|
|
697
|
+
</InlineTool>
|
|
698
|
+
</Match>
|
|
699
|
+
</Switch>
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function Glob(props: ToolProps) {
|
|
704
|
+
return (
|
|
705
|
+
<InlineTool icon="✱" pending="Finding files..." complete={toolComplete(props.part)} part={props.part}>
|
|
706
|
+
Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
|
|
707
|
+
<Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
|
|
708
|
+
<Show when={numberValue(props.metadata.count)}>
|
|
709
|
+
{(count) => (
|
|
710
|
+
<>
|
|
711
|
+
({count()} {count() === 1 ? "match" : "matches"})
|
|
712
|
+
</>
|
|
713
|
+
)}
|
|
714
|
+
</Show>
|
|
715
|
+
</InlineTool>
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function Read(props: ToolProps) {
|
|
720
|
+
const { theme } = useTheme()
|
|
721
|
+
const loaded = createMemo(() =>
|
|
722
|
+
arrayValue(props.metadata.loaded).filter((item): item is string => typeof item === "string"),
|
|
723
|
+
)
|
|
724
|
+
return (
|
|
725
|
+
<>
|
|
726
|
+
<InlineTool
|
|
727
|
+
icon="→"
|
|
728
|
+
pending="Reading file..."
|
|
729
|
+
complete={stringValue(props.input.filePath) ?? pendingInput(props.part)}
|
|
730
|
+
spinner={props.part.state.status === "running"}
|
|
731
|
+
part={props.part}
|
|
732
|
+
>
|
|
733
|
+
Read {normalizePath(stringValue(props.input.filePath) ?? pendingInput(props.part))}{" "}
|
|
734
|
+
{input(props.input, ["filePath"])}
|
|
735
|
+
</InlineTool>
|
|
736
|
+
<For each={loaded()}>
|
|
737
|
+
{(filepath) => (
|
|
738
|
+
<box paddingLeft={3} flexShrink={0}>
|
|
739
|
+
<text paddingLeft={3} fg={theme.textMuted}>
|
|
740
|
+
↳ Loaded {normalizePath(filepath)}
|
|
741
|
+
</text>
|
|
742
|
+
</box>
|
|
743
|
+
)}
|
|
744
|
+
</For>
|
|
745
|
+
</>
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function Grep(props: ToolProps) {
|
|
750
|
+
return (
|
|
751
|
+
<InlineTool icon="✱" pending="Searching content..." complete={toolComplete(props.part)} part={props.part}>
|
|
752
|
+
Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
|
|
753
|
+
<Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
|
|
754
|
+
<Show when={numberValue(props.metadata.matches)}>
|
|
755
|
+
{(matches) => (
|
|
756
|
+
<>
|
|
757
|
+
({matches()} {matches() === 1 ? "match" : "matches"})
|
|
758
|
+
</>
|
|
759
|
+
)}
|
|
760
|
+
</Show>
|
|
761
|
+
</InlineTool>
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function WebFetch(props: ToolProps) {
|
|
766
|
+
return (
|
|
767
|
+
<InlineTool icon="%" pending="Fetching from the web..." complete={toolComplete(props.part)} part={props.part}>
|
|
768
|
+
WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)}
|
|
769
|
+
</InlineTool>
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function WebSearch(props: ToolProps) {
|
|
774
|
+
const label = createMemo(() => webSearchProviderLabel(props.metadata.provider))
|
|
775
|
+
return (
|
|
776
|
+
<InlineTool icon="◈" pending="Searching web..." complete={toolComplete(props.part)} part={props.part}>
|
|
777
|
+
{label()} "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
|
|
778
|
+
<Show when={numberValue(props.metadata.numResults)}>{(results) => <>({results()} results)</>}</Show>
|
|
779
|
+
</InlineTool>
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function Write(props: ToolProps) {
|
|
784
|
+
const { theme, syntax } = useTheme()
|
|
785
|
+
const filePath = createMemo(() => stringValue(props.input.filePath) ?? "")
|
|
786
|
+
const content = createMemo(() => stringValue(props.input.content) ?? "")
|
|
787
|
+
return (
|
|
788
|
+
<Switch>
|
|
789
|
+
<Match when={content() && props.part.state.status === "completed"}>
|
|
790
|
+
<BlockTool title={"# Wrote " + normalizePath(filePath())} part={props.part}>
|
|
791
|
+
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
|
|
792
|
+
<code
|
|
793
|
+
conceal={false}
|
|
794
|
+
fg={theme.text}
|
|
795
|
+
filetype={filetype(filePath())}
|
|
796
|
+
syntaxStyle={syntax()}
|
|
797
|
+
content={content()}
|
|
798
|
+
/>
|
|
799
|
+
</line_number>
|
|
800
|
+
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={filePath()} />
|
|
801
|
+
</BlockTool>
|
|
802
|
+
</Match>
|
|
803
|
+
<Match when={true}>
|
|
804
|
+
<InlineTool icon="←" pending="Preparing write..." complete={filePath()} part={props.part}>
|
|
805
|
+
Write {normalizePath(filePath())}
|
|
806
|
+
</InlineTool>
|
|
807
|
+
</Match>
|
|
808
|
+
</Switch>
|
|
809
|
+
)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function Edit(props: ToolProps) {
|
|
813
|
+
const { theme, syntax } = useTheme()
|
|
814
|
+
const dimensions = useTerminalDimensions()
|
|
815
|
+
const filePath = createMemo(() => stringValue(props.input.filePath) ?? "")
|
|
816
|
+
const diff = createMemo(() => stringValue(props.metadata.diff))
|
|
817
|
+
return (
|
|
818
|
+
<Switch>
|
|
819
|
+
<Match when={diff()}>
|
|
820
|
+
{(diff) => (
|
|
821
|
+
<BlockTool title={"← Edit " + normalizePath(filePath())} part={props.part}>
|
|
822
|
+
<box paddingLeft={1}>
|
|
823
|
+
<diff
|
|
824
|
+
diff={diff()}
|
|
825
|
+
view={dimensions().width > 120 ? "split" : "unified"}
|
|
826
|
+
filetype={filetype(filePath())}
|
|
827
|
+
syntaxStyle={syntax()}
|
|
828
|
+
showLineNumbers={true}
|
|
829
|
+
width="100%"
|
|
830
|
+
wrapMode="word"
|
|
831
|
+
fg={theme.text}
|
|
832
|
+
addedBg={theme.diffAddedBg}
|
|
833
|
+
removedBg={theme.diffRemovedBg}
|
|
834
|
+
contextBg={theme.diffContextBg}
|
|
835
|
+
addedSignColor={theme.diffHighlightAdded}
|
|
836
|
+
removedSignColor={theme.diffHighlightRemoved}
|
|
837
|
+
lineNumberFg={theme.diffLineNumber}
|
|
838
|
+
lineNumberBg={theme.diffContextBg}
|
|
839
|
+
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
|
840
|
+
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
|
841
|
+
/>
|
|
842
|
+
</box>
|
|
843
|
+
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={filePath()} />
|
|
844
|
+
</BlockTool>
|
|
845
|
+
)}
|
|
846
|
+
</Match>
|
|
847
|
+
<Match when={true}>
|
|
848
|
+
<InlineTool icon="←" pending="Preparing edit..." complete={filePath()} part={props.part}>
|
|
849
|
+
Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })}
|
|
850
|
+
</InlineTool>
|
|
851
|
+
</Match>
|
|
852
|
+
</Switch>
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function ApplyPatch(props: ToolProps) {
|
|
857
|
+
const { theme, syntax } = useTheme()
|
|
858
|
+
const dimensions = useTerminalDimensions()
|
|
859
|
+
const files = createMemo(() => arrayValue(props.metadata.files).flatMap((item) => (isRecord(item) ? [item] : [])))
|
|
860
|
+
const fileTitle = (file: Record<string, unknown>) => {
|
|
861
|
+
const type = stringValue(file.type)
|
|
862
|
+
const relativePath = stringValue(file.relativePath) ?? stringValue(file.filePath) ?? "patch"
|
|
863
|
+
if (type === "delete") return "# Deleted " + relativePath
|
|
864
|
+
if (type === "add") return "# Created " + relativePath
|
|
865
|
+
if (type === "move") return "# Moved " + normalizePath(stringValue(file.filePath)) + " → " + relativePath
|
|
866
|
+
return "← Patched " + relativePath
|
|
867
|
+
}
|
|
868
|
+
return (
|
|
869
|
+
<Switch>
|
|
870
|
+
<Match when={files().length > 0}>
|
|
871
|
+
<For each={files()}>
|
|
872
|
+
{(file) => (
|
|
873
|
+
<BlockTool title={fileTitle(file)} part={props.part}>
|
|
874
|
+
<Show
|
|
875
|
+
when={stringValue(file.patch)}
|
|
876
|
+
fallback={
|
|
877
|
+
<text fg={theme.diffRemoved}>
|
|
878
|
+
-{numberValue(file.deletions) ?? 0} line{numberValue(file.deletions) === 1 ? "" : "s"}
|
|
879
|
+
</text>
|
|
880
|
+
}
|
|
881
|
+
>
|
|
882
|
+
{(patch) => (
|
|
883
|
+
<box paddingLeft={1}>
|
|
884
|
+
<diff
|
|
885
|
+
diff={patch()}
|
|
886
|
+
view={dimensions().width > 120 ? "split" : "unified"}
|
|
887
|
+
filetype={filetype(stringValue(file.filePath) ?? stringValue(file.relativePath))}
|
|
888
|
+
syntaxStyle={syntax()}
|
|
889
|
+
showLineNumbers={true}
|
|
890
|
+
width="100%"
|
|
891
|
+
wrapMode="word"
|
|
892
|
+
fg={theme.text}
|
|
893
|
+
addedBg={theme.diffAddedBg}
|
|
894
|
+
removedBg={theme.diffRemovedBg}
|
|
895
|
+
contextBg={theme.diffContextBg}
|
|
896
|
+
addedSignColor={theme.diffHighlightAdded}
|
|
897
|
+
removedSignColor={theme.diffHighlightRemoved}
|
|
898
|
+
lineNumberFg={theme.diffLineNumber}
|
|
899
|
+
lineNumberBg={theme.diffContextBg}
|
|
900
|
+
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
|
901
|
+
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
|
902
|
+
/>
|
|
903
|
+
</box>
|
|
904
|
+
)}
|
|
905
|
+
</Show>
|
|
906
|
+
</BlockTool>
|
|
907
|
+
)}
|
|
908
|
+
</For>
|
|
909
|
+
</Match>
|
|
910
|
+
<Match when={true}>
|
|
911
|
+
<InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
|
|
912
|
+
Patch
|
|
913
|
+
</InlineTool>
|
|
914
|
+
</Match>
|
|
915
|
+
</Switch>
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function TodoWrite(props: ToolProps) {
|
|
920
|
+
const { theme } = useTheme()
|
|
921
|
+
const todos = createMemo(() => arrayValue(props.input.todos).flatMap((item) => (isRecord(item) ? [item] : [])))
|
|
922
|
+
return (
|
|
923
|
+
<Switch>
|
|
924
|
+
<Match when={todos().length > 0 && props.part.state.status === "completed"}>
|
|
925
|
+
<BlockTool title="# Todos" part={props.part}>
|
|
926
|
+
<box>
|
|
927
|
+
<For each={todos()}>
|
|
928
|
+
{(todo) => (
|
|
929
|
+
<text fg={theme.text}>
|
|
930
|
+
{todoIcon(stringValue(todo.status))} {stringValue(todo.content)}
|
|
931
|
+
</text>
|
|
932
|
+
)}
|
|
933
|
+
</For>
|
|
934
|
+
</box>
|
|
935
|
+
</BlockTool>
|
|
936
|
+
</Match>
|
|
937
|
+
<Match when={true}>
|
|
938
|
+
<InlineTool icon="⚙" pending="Updating todos..." complete={false} part={props.part}>
|
|
939
|
+
Updating todos...
|
|
940
|
+
</InlineTool>
|
|
941
|
+
</Match>
|
|
942
|
+
</Switch>
|
|
943
|
+
)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function Question(props: ToolProps) {
|
|
947
|
+
const { theme } = useTheme()
|
|
948
|
+
const questions = createMemo(() =>
|
|
949
|
+
arrayValue(props.input.questions).flatMap((item) => (isRecord(item) ? [item] : [])),
|
|
950
|
+
)
|
|
951
|
+
const answers = createMemo(() => arrayValue(props.metadata.answers))
|
|
952
|
+
return (
|
|
953
|
+
<Switch>
|
|
954
|
+
<Match when={answers().length > 0}>
|
|
955
|
+
<BlockTool title="# Questions" part={props.part}>
|
|
956
|
+
<box gap={1}>
|
|
957
|
+
<For each={questions()}>
|
|
958
|
+
{(question, index) => (
|
|
959
|
+
<box>
|
|
960
|
+
<text fg={theme.textMuted}>{stringValue(question.question)}</text>
|
|
961
|
+
<text fg={theme.text}>{formatAnswer(answers()[index()])}</text>
|
|
962
|
+
</box>
|
|
963
|
+
)}
|
|
964
|
+
</For>
|
|
965
|
+
</box>
|
|
966
|
+
</BlockTool>
|
|
967
|
+
</Match>
|
|
968
|
+
<Match when={true}>
|
|
969
|
+
<InlineTool icon="→" pending="Asking questions..." complete={questions().length} part={props.part}>
|
|
970
|
+
Asked {questions().length} question{questions().length === 1 ? "" : "s"}
|
|
971
|
+
</InlineTool>
|
|
972
|
+
</Match>
|
|
973
|
+
</Switch>
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function Skill(props: ToolProps) {
|
|
978
|
+
return (
|
|
979
|
+
<InlineTool icon="→" pending="Loading skill..." complete={toolComplete(props.part)} part={props.part}>
|
|
980
|
+
Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}"
|
|
981
|
+
</InlineTool>
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function Task(props: ToolProps) {
|
|
986
|
+
const content = createMemo(() => {
|
|
987
|
+
const description = stringValue(props.input.description)
|
|
988
|
+
if (!description) return pendingInput(props.part)
|
|
989
|
+
return `${Locale.titlecase(stringValue(props.input.subagent_type) ?? "General")} Task — ${description}`
|
|
990
|
+
})
|
|
991
|
+
return (
|
|
992
|
+
<InlineTool
|
|
993
|
+
icon="│"
|
|
994
|
+
spinner={props.part.state.status === "running"}
|
|
995
|
+
complete={toolComplete(props.part)}
|
|
996
|
+
pending="Delegating..."
|
|
997
|
+
part={props.part}
|
|
998
|
+
>
|
|
999
|
+
{content()}
|
|
1000
|
+
</InlineTool>
|
|
1001
|
+
)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
|
|
1005
|
+
const { theme } = useTheme()
|
|
1006
|
+
const errors = createMemo(() => {
|
|
1007
|
+
if (!isRecord(props.diagnostics)) return []
|
|
1008
|
+
const value = props.diagnostics[normalizePath(props.filePath)] ?? props.diagnostics[props.filePath]
|
|
1009
|
+
return arrayValue(value)
|
|
1010
|
+
.flatMap((item) => (isRecord(item) ? [item] : []))
|
|
1011
|
+
.filter((diagnostic) => diagnostic.severity === 1)
|
|
1012
|
+
.slice(0, 3)
|
|
1013
|
+
})
|
|
1014
|
+
return (
|
|
1015
|
+
<Show when={errors().length}>
|
|
1016
|
+
<box>
|
|
1017
|
+
<For each={errors()}>
|
|
1018
|
+
{(diagnostic) => <text fg={theme.error}>Error {stringValue(diagnostic.message)}</text>}
|
|
1019
|
+
</For>
|
|
1020
|
+
</box>
|
|
1021
|
+
</Show>
|
|
1022
|
+
)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
|
|
1026
|
+
return (content ?? [])
|
|
1027
|
+
.map((item) => {
|
|
1028
|
+
if (item.type === "text") return item.text.trim()
|
|
1029
|
+
return `[file ${item.name ?? item.uri}]`
|
|
1030
|
+
})
|
|
1031
|
+
.filter(Boolean)
|
|
1032
|
+
.join("\n")
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function toolInputRecord(input: string | Record<string, unknown>) {
|
|
1036
|
+
if (typeof input === "string") return {}
|
|
1037
|
+
return input
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function pendingInput(part: SessionMessageAssistantTool) {
|
|
1041
|
+
if (part.state.status !== "pending") return ""
|
|
1042
|
+
return part.state.input.trim()
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function toolComplete(part: SessionMessageAssistantTool) {
|
|
1046
|
+
if (part.state.status === "pending") return pendingInput(part)
|
|
1047
|
+
return part.state.status === "completed" || part.state.status === "error" || part.state.status === "running"
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function stringValue(value: unknown) {
|
|
1051
|
+
return typeof value === "string" ? value : undefined
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function numberValue(value: unknown) {
|
|
1055
|
+
return typeof value === "number" ? value : undefined
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function arrayValue(value: unknown): unknown[] {
|
|
1059
|
+
return Array.isArray(value) ? value : []
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1063
|
+
return !!value && typeof value === "object" && !Array.isArray(value)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function input(input: Record<string, unknown>, omit?: string[]) {
|
|
1067
|
+
const primitives = Object.entries(input).filter(([key, value]) => {
|
|
1068
|
+
if (omit?.includes(key)) return false
|
|
1069
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
1070
|
+
})
|
|
1071
|
+
if (primitives.length === 0) return ""
|
|
1072
|
+
return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]`
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function normalizePath(input?: string) {
|
|
1076
|
+
if (!input) return ""
|
|
1077
|
+
const absolute = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input)
|
|
1078
|
+
const relative = path.relative(process.cwd(), absolute)
|
|
1079
|
+
if (!relative) return "."
|
|
1080
|
+
if (!relative.startsWith("..")) return relative
|
|
1081
|
+
return absolute
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function filetype(input?: string) {
|
|
1085
|
+
if (!input) return "none"
|
|
1086
|
+
const language = LANGUAGE_EXTENSIONS[path.extname(input)]
|
|
1087
|
+
if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
|
|
1088
|
+
return language
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function todoIcon(status?: string) {
|
|
1092
|
+
if (status === "completed") return "✓"
|
|
1093
|
+
if (status === "in_progress") return "~"
|
|
1094
|
+
if (status === "cancelled") return "✕"
|
|
1095
|
+
return "☐"
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function formatAnswer(answer: unknown) {
|
|
1099
|
+
if (!Array.isArray(answer)) return "(no answer)"
|
|
1100
|
+
if (answer.length === 0) return "(no answer)"
|
|
1101
|
+
return answer.filter((item): item is string => typeof item === "string").join(", ")
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const tui: TuiPlugin = async (api) => {
|
|
1105
|
+
api.route.register([
|
|
1106
|
+
{
|
|
1107
|
+
name: route,
|
|
1108
|
+
render(input) {
|
|
1109
|
+
const sessionID = input.params?.sessionID
|
|
1110
|
+
if (typeof sessionID !== "string") {
|
|
1111
|
+
return <text fg={api.theme.current.error}>Missing sessionID</text>
|
|
1112
|
+
}
|
|
1113
|
+
return <View api={api} sessionID={sessionID} />
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
])
|
|
1117
|
+
|
|
1118
|
+
api.keymap.registerLayer({
|
|
1119
|
+
commands: [
|
|
1120
|
+
{
|
|
1121
|
+
name: route,
|
|
1122
|
+
title: "View v2 session messages",
|
|
1123
|
+
category: "Debug",
|
|
1124
|
+
namespace: "palette",
|
|
1125
|
+
suggested: () => api.route.current.name === "session",
|
|
1126
|
+
enabled: () => api.route.current.name === "session",
|
|
1127
|
+
run() {
|
|
1128
|
+
const sessionID = currentSessionID(api)
|
|
1129
|
+
if (!sessionID) return
|
|
1130
|
+
api.route.navigate(route, { sessionID })
|
|
1131
|
+
api.ui.dialog.clear()
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
],
|
|
1135
|
+
})
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const plugin: InternalTuiPlugin = {
|
|
1139
|
+
id,
|
|
1140
|
+
tui,
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export default plugin
|