herm-tui 1.0.0-dev.1 → 1.0.0-dev.3
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/db.worker.js +81 -0
- package/highlights-eq9cgrbb.scm +604 -0
- package/highlights-ghv9g403.scm +205 -0
- package/highlights-hk7bwhj4.scm +284 -0
- package/highlights-r812a2qc.scm +150 -0
- package/highlights-x6tmsnaa.scm +115 -0
- package/index.js +10374 -0
- package/injections-73j83es3.scm +27 -0
- package/package.json +14 -64
- package/parser.worker.js +8 -0
- package/tree-sitter-3jzf13jk.wasm +0 -0
- package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/scripts/postinstall.ts +0 -29
- package/src/app/gateway.tsx +0 -83
- package/src/app/gatewayEvents.ts +0 -203
- package/src/app/launch.ts +0 -41
- package/src/app/skin.tsx +0 -31
- package/src/app/spawnHistory.ts +0 -75
- package/src/app/tabs.ts +0 -23
- package/src/app/turnReducer.ts +0 -390
- package/src/app/useAppKeys.ts +0 -268
- package/src/app/useAtRefPopover.ts +0 -99
- package/src/app/useInputHistory.ts +0 -66
- package/src/app/useSession.ts +0 -102
- package/src/app/useSlashCommands.ts +0 -70
- package/src/app/useSlashPopover.ts +0 -48
- package/src/app.tsx +0 -917
- package/src/commands/slash.ts +0 -151
- package/src/components/avatar/AnimatedAvatar.tsx +0 -66
- package/src/components/avatar/eikon.ts +0 -144
- package/src/components/avatar/states/error.ts +0 -1155
- package/src/components/avatar/states/idle.ts +0 -1155
- package/src/components/avatar/states/index.ts +0 -30
- package/src/components/avatar/states/listening.ts +0 -1155
- package/src/components/avatar/states/speaking.ts +0 -1155
- package/src/components/avatar/states/thinking.ts +0 -1155
- package/src/components/avatar/states/working.ts +0 -1155
- package/src/components/chat/AtRefPopover.tsx +0 -54
- package/src/components/chat/CodeBlock.tsx +0 -67
- package/src/components/chat/Composer.tsx +0 -347
- package/src/components/chat/DiffBlock.tsx +0 -116
- package/src/components/chat/ErrorBlock.tsx +0 -70
- package/src/components/chat/MediaChip.tsx +0 -114
- package/src/components/chat/MessageItem.tsx +0 -282
- package/src/components/chat/MessageList.tsx +0 -114
- package/src/components/chat/PromptCard.tsx +0 -359
- package/src/components/chat/SlashPopover.tsx +0 -158
- package/src/components/chat/ThoughtCloud.tsx +0 -185
- package/src/components/chat/TypingIndicator.tsx +0 -25
- package/src/components/chat/tool/Subagent.tsx +0 -75
- package/src/components/chat/tool/frame.tsx +0 -69
- package/src/components/chat/tool/index.tsx +0 -65
- package/src/components/chat/tool/preview.ts +0 -57
- package/src/components/sidebar/ContextGauge.tsx +0 -102
- package/src/components/sidebar/Sidebar.tsx +0 -143
- package/src/components/tabs/TabBar.tsx +0 -50
- package/src/components/ui/FileLink.tsx +0 -52
- package/src/config/index.ts +0 -156
- package/src/config/lane.ts +0 -161
- package/src/config/models.ts +0 -95
- package/src/config/rules.ts +0 -80
- package/src/config/schema.ts +0 -308
- package/src/dialogs/alert.tsx +0 -52
- package/src/dialogs/chafa.tsx +0 -72
- package/src/dialogs/confirm.tsx +0 -58
- package/src/dialogs/curator.tsx +0 -153
- package/src/dialogs/eikon-picker.tsx +0 -95
- package/src/dialogs/help.tsx +0 -80
- package/src/dialogs/history.tsx +0 -92
- package/src/dialogs/info.tsx +0 -115
- package/src/dialogs/keys.tsx +0 -170
- package/src/dialogs/logs.tsx +0 -42
- package/src/dialogs/message.tsx +0 -38
- package/src/dialogs/model-picker.tsx +0 -123
- package/src/dialogs/new-profile.tsx +0 -69
- package/src/dialogs/new-task.tsx +0 -103
- package/src/dialogs/profile.tsx +0 -55
- package/src/dialogs/rollback.tsx +0 -190
- package/src/dialogs/spawn-history.tsx +0 -80
- package/src/dialogs/text-prompt.tsx +0 -68
- package/src/dialogs/theme-picker.tsx +0 -50
- package/src/home/index.ts +0 -23
- package/src/home/store.ts +0 -267
- package/src/index.tsx +0 -113
- package/src/keys/catalog.ts +0 -115
- package/src/keys/chord.ts +0 -125
- package/src/keys/conflicts.ts +0 -48
- package/src/keys/context.tsx +0 -112
- package/src/keys/index.ts +0 -5
- package/src/keys/list.ts +0 -94
- package/src/keys/oc-compat.ts +0 -87
- package/src/tabs/Agents.tsx +0 -607
- package/src/tabs/Analytics.tsx +0 -154
- package/src/tabs/Chat.tsx +0 -50
- package/src/tabs/Config.tsx +0 -605
- package/src/tabs/Context.tsx +0 -599
- package/src/tabs/Cron.tsx +0 -294
- package/src/tabs/Env.tsx +0 -227
- package/src/tabs/Kanban.tsx +0 -367
- package/src/tabs/Memory.tsx +0 -294
- package/src/tabs/Sessions.tsx +0 -786
- package/src/tabs/Skills.tsx +0 -507
- package/src/tabs/Toolsets.tsx +0 -266
- package/src/theme/builtin.ts +0 -78
- package/src/theme/context.tsx +0 -106
- package/src/theme/index.ts +0 -4
- package/src/theme/resolve.ts +0 -134
- package/src/theme/syntax.ts +0 -31
- package/src/theme/themes/aura.json +0 -69
- package/src/theme/themes/ayu.json +0 -80
- package/src/theme/themes/carbonfox.json +0 -248
- package/src/theme/themes/catppuccin-frappe.json +0 -233
- package/src/theme/themes/catppuccin-macchiato.json +0 -233
- package/src/theme/themes/catppuccin.json +0 -112
- package/src/theme/themes/cobalt2.json +0 -228
- package/src/theme/themes/cursor.json +0 -249
- package/src/theme/themes/dracula.json +0 -219
- package/src/theme/themes/everforest.json +0 -241
- package/src/theme/themes/flexoki.json +0 -237
- package/src/theme/themes/github.json +0 -233
- package/src/theme/themes/gruvbox.json +0 -242
- package/src/theme/themes/kanagawa.json +0 -77
- package/src/theme/themes/lucent-orng.json +0 -237
- package/src/theme/themes/material.json +0 -235
- package/src/theme/themes/matrix.json +0 -77
- package/src/theme/themes/mercury.json +0 -252
- package/src/theme/themes/monokai.json +0 -221
- package/src/theme/themes/nightowl.json +0 -221
- package/src/theme/themes/nord.json +0 -223
- package/src/theme/themes/one-dark.json +0 -84
- package/src/theme/themes/opencode.json +0 -245
- package/src/theme/themes/orng.json +0 -249
- package/src/theme/themes/osaka-jade.json +0 -93
- package/src/theme/themes/palenight.json +0 -222
- package/src/theme/themes/rosepine.json +0 -234
- package/src/theme/themes/solarized.json +0 -223
- package/src/theme/themes/synthwave84.json +0 -226
- package/src/theme/themes/tokyonight.json +0 -243
- package/src/theme/themes/vercel.json +0 -245
- package/src/theme/themes/vesper.json +0 -218
- package/src/theme/themes/zenburn.json +0 -223
- package/src/theme/types.ts +0 -119
- package/src/types/message.ts +0 -97
- package/src/ui/ChafaImage.tsx +0 -64
- package/src/ui/Splash.tsx +0 -118
- package/src/ui/borders.ts +0 -28
- package/src/ui/command.tsx +0 -104
- package/src/ui/dialog-select.tsx +0 -164
- package/src/ui/dialog.tsx +0 -102
- package/src/ui/fmt.ts +0 -82
- package/src/ui/kv.tsx +0 -28
- package/src/ui/shell.tsx +0 -45
- package/src/ui/spinner.tsx +0 -59
- package/src/ui/splash-art.ts +0 -123
- package/src/ui/table.tsx +0 -117
- package/src/ui/ticker.tsx +0 -90
- package/src/ui/toast.tsx +0 -130
- package/src/utils/categorical.ts +0 -77
- package/src/utils/chafa.ts +0 -173
- package/src/utils/clipboard.ts +0 -67
- package/src/utils/context-segments.ts +0 -317
- package/src/utils/control.ts +0 -495
- package/src/utils/drop.ts +0 -25
- package/src/utils/editor.ts +0 -33
- package/src/utils/fuzzy.ts +0 -45
- package/src/utils/gateway-client.ts +0 -253
- package/src/utils/gateway-types.ts +0 -282
- package/src/utils/git.ts +0 -57
- package/src/utils/hermes-analytics.ts +0 -134
- package/src/utils/hermes-home.ts +0 -821
- package/src/utils/hermes-kanban.ts +0 -154
- package/src/utils/hermes-profiles.ts +0 -217
- package/src/utils/interpolate.ts +0 -31
- package/src/utils/math-unicode.ts +0 -818
- package/src/utils/memory-activity.ts +0 -140
- package/src/utils/open-file.ts +0 -13
- package/src/utils/paths.ts +0 -52
- package/src/utils/perf.ts +0 -235
- package/src/utils/preferences.ts +0 -150
- package/src/utils/sessions-db.ts +0 -396
- package/src/utils/subagent-tree.ts +0 -146
- package/src/utils/terminal-reset.ts +0 -129
- package/src/utils/tips.ts +0 -67
- package/src/utils/tokens.ts +0 -87
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
// Inline agent prompts — approval / clarify / sudo / secret.
|
|
2
|
-
//
|
|
3
|
-
// These render *in the transcript* as a Part of the in-progress
|
|
4
|
-
// assistant message, not in a modal. The composer stays focused for
|
|
5
|
-
// approval/clarify; the shell's global key handler routes keys to
|
|
6
|
-
// the pending card via the imperative handle so number/arrow/Enter
|
|
7
|
-
// work without the textarea eating them. Sudo/secret own a masked
|
|
8
|
-
// <input> and take focus explicitly (the value must never echo into
|
|
9
|
-
// the composer).
|
|
10
|
-
//
|
|
11
|
-
// Responding is exactly-once per card but NOT unmount-triggered — the
|
|
12
|
-
// card can scroll out of the viewport (culling) without auto-denying.
|
|
13
|
-
// Esc is the only cancel path.
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
memo, useRef, useState, forwardRef, useImperativeHandle,
|
|
17
|
-
} from "react"
|
|
18
|
-
import { LEFT_BAR } from "../../ui/borders"
|
|
19
|
-
import type { ParsedKey, SubmitEvent } from "@opentui/core"
|
|
20
|
-
import { useTheme } from "../../theme"
|
|
21
|
-
import { useGateway } from "../../app/gateway"
|
|
22
|
-
import type { PromptPart, PromptReq, Part } from "../../types/message"
|
|
23
|
-
|
|
24
|
-
// ── Shared ───────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
export type PromptCardHandle = {
|
|
27
|
-
/** Offer a key to the pending card. Returns true if consumed. */
|
|
28
|
-
feed: (key: ParsedKey) => boolean
|
|
29
|
-
/** True if this card owns a focused <input> (sudo/secret). */
|
|
30
|
-
masked: boolean
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type Answer = (label: string, ok: boolean) => void
|
|
34
|
-
|
|
35
|
-
function digit(name: string): number | null {
|
|
36
|
-
const n = parseInt(name, 10)
|
|
37
|
-
return Number.isFinite(n) ? n : null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ┃-bar panel frame — matches the oc permission grammar that prompts
|
|
41
|
-
// already used inside the modal, minus the fixed width.
|
|
42
|
-
const Frame = (p: { tint: import("@opentui/core").RGBA; children: React.ReactNode }) => {
|
|
43
|
-
const theme = useTheme().theme
|
|
44
|
-
return (
|
|
45
|
-
<box
|
|
46
|
-
flexDirection="column"
|
|
47
|
-
border={["left"]}
|
|
48
|
-
borderColor={p.tint}
|
|
49
|
-
customBorderChars={LEFT_BAR}
|
|
50
|
-
backgroundColor={theme.backgroundPanel}
|
|
51
|
-
marginBottom={1}
|
|
52
|
-
>
|
|
53
|
-
{p.children}
|
|
54
|
-
</box>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const Pill = (p: { on: boolean; hot: string; label: string; onPick: () => void }) => {
|
|
59
|
-
const theme = useTheme().theme
|
|
60
|
-
return (
|
|
61
|
-
<box height={1} paddingX={1}
|
|
62
|
-
backgroundColor={p.on ? theme.primary : undefined}
|
|
63
|
-
onMouseDown={p.onPick}>
|
|
64
|
-
<text>
|
|
65
|
-
<span fg={p.on ? theme.background : theme.textMuted}>{p.hot} </span>
|
|
66
|
-
<span fg={p.on ? theme.background : theme.text}>{p.label}</span>
|
|
67
|
-
</text>
|
|
68
|
-
</box>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Approval ─────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
const CHOICES = ["once", "session", "always", "deny"] as const
|
|
75
|
-
type Choice = typeof CHOICES[number]
|
|
76
|
-
const LABELS: Record<Choice, string> = {
|
|
77
|
-
once: "Allow once",
|
|
78
|
-
session: "Allow this session",
|
|
79
|
-
always: "Always allow",
|
|
80
|
-
deny: "Deny",
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const Approval = forwardRef<PromptCardHandle, {
|
|
84
|
-
req: Extract<PromptReq, { variant: "approval" }>
|
|
85
|
-
onAnswer: Answer
|
|
86
|
-
}>((p, ref) => {
|
|
87
|
-
const theme = useTheme().theme
|
|
88
|
-
const gw = useGateway()
|
|
89
|
-
const [sel, setSel] = useState(0)
|
|
90
|
-
const done = useRef(false)
|
|
91
|
-
|
|
92
|
-
const send = (c: Choice) => {
|
|
93
|
-
if (done.current) return
|
|
94
|
-
done.current = true
|
|
95
|
-
void gw.request("approval.respond", { choice: c }).catch(() => {})
|
|
96
|
-
p.onAnswer(LABELS[c], c !== "deny")
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
useImperativeHandle(ref, () => ({
|
|
100
|
-
masked: false,
|
|
101
|
-
feed: (key) => {
|
|
102
|
-
if (key.name === "left" || key.name === "h") {
|
|
103
|
-
setSel(s => (s + CHOICES.length - 1) % CHOICES.length); return true
|
|
104
|
-
}
|
|
105
|
-
if (key.name === "right" || key.name === "l") {
|
|
106
|
-
setSel(s => (s + 1) % CHOICES.length); return true
|
|
107
|
-
}
|
|
108
|
-
if (key.name === "return") { send(CHOICES[sel]); return true }
|
|
109
|
-
if (key.name === "escape") { send("deny"); return true }
|
|
110
|
-
const n = digit(key.name)
|
|
111
|
-
if (n !== null && n >= 1 && n <= CHOICES.length) { send(CHOICES[n - 1]); return true }
|
|
112
|
-
return false
|
|
113
|
-
},
|
|
114
|
-
}), [sel])
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<Frame tint={theme.warning}>
|
|
118
|
-
<box flexDirection="column" gap={1} paddingLeft={1} paddingRight={2} paddingY={1}>
|
|
119
|
-
<box flexDirection="row" gap={1} height={1}>
|
|
120
|
-
<text fg={theme.warning}>△</text>
|
|
121
|
-
<text fg={theme.text}>Permission required</text>
|
|
122
|
-
</box>
|
|
123
|
-
<box flexDirection="row" gap={1} paddingLeft={2} minHeight={1}>
|
|
124
|
-
<text fg={theme.textMuted}>#</text>
|
|
125
|
-
<text fg={theme.text} wrapMode="word">{p.req.description || "Shell command"}</text>
|
|
126
|
-
</box>
|
|
127
|
-
<box paddingLeft={2} minHeight={1}>
|
|
128
|
-
<text fg={theme.text} wrapMode="word">$ {p.req.command}</text>
|
|
129
|
-
</box>
|
|
130
|
-
</box>
|
|
131
|
-
<box flexDirection="row" gap={2} flexShrink={0}
|
|
132
|
-
paddingX={2} paddingY={1} backgroundColor={theme.backgroundElement}>
|
|
133
|
-
{CHOICES.map((c, i) => (
|
|
134
|
-
<Pill key={c} on={sel === i} hot={String(i + 1)} label={LABELS[c]}
|
|
135
|
-
onPick={() => send(c)} />
|
|
136
|
-
))}
|
|
137
|
-
<box flexGrow={1} />
|
|
138
|
-
<box height={1}>
|
|
139
|
-
<text fg={theme.textMuted}>←/→ · enter · esc deny</text>
|
|
140
|
-
</box>
|
|
141
|
-
</box>
|
|
142
|
-
</Frame>
|
|
143
|
-
)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// ── Clarify ──────────────────────────────────────────────────────────
|
|
147
|
-
|
|
148
|
-
const Clarify = forwardRef<PromptCardHandle, {
|
|
149
|
-
req: Extract<PromptReq, { variant: "clarify" }>
|
|
150
|
-
onAnswer: Answer
|
|
151
|
-
}>((p, ref) => {
|
|
152
|
-
const theme = useTheme().theme
|
|
153
|
-
const gw = useGateway()
|
|
154
|
-
const choices = p.req.choices ?? []
|
|
155
|
-
const [sel, setSel] = useState(0)
|
|
156
|
-
const [typing, setTyping] = useState(choices.length === 0)
|
|
157
|
-
const [custom, setCustom] = useState("")
|
|
158
|
-
const done = useRef(false)
|
|
159
|
-
|
|
160
|
-
const send = (answer: string) => {
|
|
161
|
-
if (done.current) return
|
|
162
|
-
done.current = true
|
|
163
|
-
void gw.request("clarify.respond", {
|
|
164
|
-
request_id: p.req.request_id, answer,
|
|
165
|
-
}).catch(() => {})
|
|
166
|
-
p.onAnswer(answer || "(cancelled)", answer !== "")
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
useImperativeHandle(ref, () => ({
|
|
170
|
-
// Freeform mode owns a focused <input>; list mode doesn't.
|
|
171
|
-
masked: typing,
|
|
172
|
-
feed: (key) => {
|
|
173
|
-
if (typing) {
|
|
174
|
-
// <input> handles text; we only intercept cancel-back.
|
|
175
|
-
if (key.name === "escape") {
|
|
176
|
-
if (choices.length) { setTyping(false); return true }
|
|
177
|
-
send(""); return true
|
|
178
|
-
}
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
if (key.name === "escape") { send(""); return true }
|
|
182
|
-
if (key.name === "up") { setSel(s => Math.max(0, s - 1)); return true }
|
|
183
|
-
if (key.name === "down") { setSel(s => Math.min(choices.length, s + 1)); return true }
|
|
184
|
-
if (key.name === "return") {
|
|
185
|
-
if (sel === choices.length) { setTyping(true); return true }
|
|
186
|
-
const c = choices[sel]
|
|
187
|
-
if (c) send(c)
|
|
188
|
-
return true
|
|
189
|
-
}
|
|
190
|
-
const n = digit(key.name)
|
|
191
|
-
if (n !== null && n >= 1 && n <= choices.length) { send(choices[n - 1]); return true }
|
|
192
|
-
return false
|
|
193
|
-
},
|
|
194
|
-
}), [typing, sel, choices])
|
|
195
|
-
|
|
196
|
-
const head = (
|
|
197
|
-
<box minHeight={1}>
|
|
198
|
-
<text wrapMode="word">
|
|
199
|
-
<span fg={theme.accent}><strong>ask </strong></span>
|
|
200
|
-
<span fg={theme.text}><strong>{p.req.question}</strong></span>
|
|
201
|
-
</text>
|
|
202
|
-
</box>
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<Frame tint={theme.accent}>
|
|
207
|
-
<box flexDirection="column" paddingLeft={1} paddingRight={2} paddingY={1}>
|
|
208
|
-
{head}
|
|
209
|
-
<box height={1} />
|
|
210
|
-
{typing ? (
|
|
211
|
-
<>
|
|
212
|
-
<box flexDirection="row" height={1}>
|
|
213
|
-
<text fg={theme.textMuted}>{"> "}</text>
|
|
214
|
-
<input
|
|
215
|
-
value={custom} onInput={setCustom}
|
|
216
|
-
onSubmit={(() => send(custom)) as unknown as (e: SubmitEvent) => void}
|
|
217
|
-
focused flexGrow={1}
|
|
218
|
-
textColor={theme.text}
|
|
219
|
-
backgroundColor={theme.backgroundElement}
|
|
220
|
-
focusedBackgroundColor={theme.backgroundElement}
|
|
221
|
-
/>
|
|
222
|
-
</box>
|
|
223
|
-
<text fg={theme.textMuted}>Enter send · Esc {choices.length ? "back" : "cancel"}</text>
|
|
224
|
-
</>
|
|
225
|
-
) : (
|
|
226
|
-
<>
|
|
227
|
-
{[...choices, "Other (type your answer)"].map((c, i) => (
|
|
228
|
-
<box key={i} height={1} onMouseDown={() =>
|
|
229
|
-
i === choices.length ? setTyping(true) : send(choices[i])}>
|
|
230
|
-
<text fg={sel === i ? theme.text : theme.textMuted}>
|
|
231
|
-
{sel === i ? "▸ " : " "}{i + 1}. {c}
|
|
232
|
-
</text>
|
|
233
|
-
</box>
|
|
234
|
-
))}
|
|
235
|
-
<box height={1} />
|
|
236
|
-
<text fg={theme.textMuted}>↑/↓ · Enter · 1-{choices.length} · Esc cancel</text>
|
|
237
|
-
</>
|
|
238
|
-
)}
|
|
239
|
-
</box>
|
|
240
|
-
</Frame>
|
|
241
|
-
)
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
// ── Masked (sudo / secret) ───────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
const Masked = forwardRef<PromptCardHandle, {
|
|
247
|
-
title: string
|
|
248
|
-
note: string
|
|
249
|
-
onSubmit: (v: string) => void
|
|
250
|
-
onAnswer: Answer
|
|
251
|
-
}>((p, ref) => {
|
|
252
|
-
const theme = useTheme().theme
|
|
253
|
-
const [value, setValue] = useState("")
|
|
254
|
-
const done = useRef(false)
|
|
255
|
-
|
|
256
|
-
const go = (v: string) => {
|
|
257
|
-
if (done.current) return
|
|
258
|
-
done.current = true
|
|
259
|
-
p.onSubmit(v)
|
|
260
|
-
p.onAnswer(v ? "(provided)" : "(cancelled)", v !== "")
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
useImperativeHandle(ref, () => ({
|
|
264
|
-
masked: true,
|
|
265
|
-
feed: (key) => {
|
|
266
|
-
if (key.name === "escape") { go(""); return true }
|
|
267
|
-
return false
|
|
268
|
-
},
|
|
269
|
-
}), [])
|
|
270
|
-
|
|
271
|
-
return (
|
|
272
|
-
<Frame tint={theme.warning}>
|
|
273
|
-
<box flexDirection="column" paddingLeft={1} paddingRight={2} paddingY={1}>
|
|
274
|
-
<text fg={theme.warning}><strong>{p.title}</strong></text>
|
|
275
|
-
<text fg={theme.text}>{p.note}</text>
|
|
276
|
-
<box height={1} />
|
|
277
|
-
<box flexDirection="row" height={1} position="relative">
|
|
278
|
-
<text fg={theme.textMuted}>{"> "}</text>
|
|
279
|
-
<input
|
|
280
|
-
value={value} onInput={setValue}
|
|
281
|
-
onSubmit={(() => go(value)) as unknown as (e: SubmitEvent) => void}
|
|
282
|
-
focused flexGrow={1}
|
|
283
|
-
textColor={theme.backgroundElement}
|
|
284
|
-
cursorColor={theme.accent}
|
|
285
|
-
backgroundColor={theme.backgroundElement}
|
|
286
|
-
focusedBackgroundColor={theme.backgroundElement}
|
|
287
|
-
/>
|
|
288
|
-
<box position="absolute" left={2} top={0} height={1}>
|
|
289
|
-
<text fg={theme.text} bg={theme.backgroundElement}>{"•".repeat(value.length)}</text>
|
|
290
|
-
</box>
|
|
291
|
-
</box>
|
|
292
|
-
<text fg={theme.textMuted}>Enter submit · Esc cancel</text>
|
|
293
|
-
</box>
|
|
294
|
-
</Frame>
|
|
295
|
-
)
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
// ── Answered (collapsed) ─────────────────────────────────────────────
|
|
299
|
-
|
|
300
|
-
const Outcome = memo(({ part }: { part: PromptPart }) => {
|
|
301
|
-
const theme = useTheme().theme
|
|
302
|
-
const a = part.answered!
|
|
303
|
-
const glyph = a.ok ? "✓" : "✗"
|
|
304
|
-
const fg = a.ok ? theme.success : theme.error
|
|
305
|
-
const what =
|
|
306
|
-
part.variant === "approval" ? a.label
|
|
307
|
-
: part.variant === "clarify" ? `chose: ${a.label}`
|
|
308
|
-
: part.variant === "sudo" ? `sudo ${a.label}`
|
|
309
|
-
: `${(part.req as { env_var?: string }).env_var ?? "secret"} ${a.label}`
|
|
310
|
-
return (
|
|
311
|
-
<box height={1} paddingLeft={3} marginBottom={1}>
|
|
312
|
-
<text>
|
|
313
|
-
<span fg={fg}>{glyph} </span>
|
|
314
|
-
<span fg={theme.textMuted}>{what}</span>
|
|
315
|
-
</text>
|
|
316
|
-
</box>
|
|
317
|
-
)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
// ── Dispatch ─────────────────────────────────────────────────────────
|
|
321
|
-
|
|
322
|
-
export const PromptCard = memo(forwardRef<PromptCardHandle, {
|
|
323
|
-
part: PromptPart
|
|
324
|
-
onAnswer: (id: string, label: string, ok: boolean) => void
|
|
325
|
-
}>((p, ref) => {
|
|
326
|
-
const gw = useGateway()
|
|
327
|
-
if (p.part.answered) return <Outcome part={p.part} />
|
|
328
|
-
const answer: Answer = (label, ok) => p.onAnswer(p.part.id, label, ok)
|
|
329
|
-
const req = p.part.req
|
|
330
|
-
if (req.variant === "approval")
|
|
331
|
-
return <Approval ref={ref} req={req} onAnswer={answer} />
|
|
332
|
-
if (req.variant === "clarify")
|
|
333
|
-
return <Clarify ref={ref} req={req} onAnswer={answer} />
|
|
334
|
-
if (req.variant === "sudo")
|
|
335
|
-
return <Masked ref={ref} title="🔒 Sudo required"
|
|
336
|
-
note="Enter your password to elevate privileges."
|
|
337
|
-
onSubmit={v => void gw.request("sudo.respond",
|
|
338
|
-
{ request_id: req.request_id, password: v }).catch(() => {})}
|
|
339
|
-
onAnswer={answer} />
|
|
340
|
-
return <Masked ref={ref} title={`🔑 Secret: ${req.env_var}`}
|
|
341
|
-
note={req.prompt}
|
|
342
|
-
onSubmit={v => void gw.request("secret.respond",
|
|
343
|
-
{ request_id: req.request_id, value: v }).catch(() => {})}
|
|
344
|
-
onAnswer={answer} />
|
|
345
|
-
}))
|
|
346
|
-
|
|
347
|
-
/** Find the single pending prompt across all messages. The gateway
|
|
348
|
-
* blocks on the answer, so there's at most one. */
|
|
349
|
-
export function pending(messages: ReadonlyArray<{ role: string; parts: ReadonlyArray<Part> }>): PromptPart | null {
|
|
350
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
351
|
-
const m = messages[i]
|
|
352
|
-
if (m.role !== "assistant") continue
|
|
353
|
-
for (let j = m.parts.length - 1; j >= 0; j--) {
|
|
354
|
-
const part = m.parts[j]
|
|
355
|
-
if (part.type === "prompt" && !part.answered) return part
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return null
|
|
359
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slash command popover — OpenCode-inspired visual style.
|
|
3
|
-
*
|
|
4
|
-
* Purely presentational. Keyboard navigation lives in the parent (app.tsx
|
|
5
|
-
* useKeyboard) to avoid OpenTUI's global keyboard event conflicts.
|
|
6
|
-
*
|
|
7
|
-
* Uses a sliding window that follows the cursor rather than scrollbox
|
|
8
|
-
* (scrollbox requires focus to scroll, which would conflict with the input).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { useMemo, memo } from "react"
|
|
12
|
-
import type { RGBA } from "@opentui/core"
|
|
13
|
-
import { useTheme } from "../../theme"
|
|
14
|
-
import type { Theme } from "../../theme"
|
|
15
|
-
import type { SlashCommand, SlashSource } from "../../commands/slash"
|
|
16
|
-
import { sort } from "../../commands/slash"
|
|
17
|
-
|
|
18
|
-
type Props = {
|
|
19
|
-
readonly commands: ReadonlyArray<SlashCommand>
|
|
20
|
-
readonly cursor: number
|
|
21
|
-
readonly onCursor: (idx: number) => void
|
|
22
|
-
readonly onSelect: (cmd: SlashCommand) => void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type Row =
|
|
26
|
-
| { type: "header"; cat: string }
|
|
27
|
-
| { type: "cmd"; cmd: SlashCommand; flat: number }
|
|
28
|
-
|
|
29
|
-
const MAX_VISIBLE = 14
|
|
30
|
-
|
|
31
|
-
/** Color for the source badge. Returns null for sources that shouldn't render. */
|
|
32
|
-
function badge(source: SlashSource, theme: Theme): RGBA | null {
|
|
33
|
-
if (source === "skill") return theme.success
|
|
34
|
-
if (source === "plugin") return theme.info
|
|
35
|
-
if (source === "mcp") return theme.warning
|
|
36
|
-
return null // "command" and "local" get no badge
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const SlashPopover = memo(({ commands: cmds, cursor, onCursor, onSelect }: Props) => {
|
|
40
|
-
const theme = useTheme().theme
|
|
41
|
-
|
|
42
|
-
if (cmds.length === 0) {
|
|
43
|
-
return (
|
|
44
|
-
<box
|
|
45
|
-
border
|
|
46
|
-
borderStyle="single"
|
|
47
|
-
borderColor={theme.border}
|
|
48
|
-
backgroundColor={theme.backgroundPanel}
|
|
49
|
-
paddingX={1}
|
|
50
|
-
height={3}
|
|
51
|
-
>
|
|
52
|
-
<text fg={theme.textMuted}>No matching commands</text>
|
|
53
|
-
</box>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Build flat row list with category headers, stable order (sort by category).
|
|
58
|
-
const rows = useMemo(() => {
|
|
59
|
-
const sorted = sort(cmds)
|
|
60
|
-
const result: Row[] = []
|
|
61
|
-
let flat = 0
|
|
62
|
-
let lastCat = ""
|
|
63
|
-
for (const cmd of sorted) {
|
|
64
|
-
if (cmd.category !== lastCat) {
|
|
65
|
-
result.push({ type: "header", cat: cmd.category })
|
|
66
|
-
lastCat = cmd.category
|
|
67
|
-
}
|
|
68
|
-
result.push({ type: "cmd", cmd, flat: flat++ })
|
|
69
|
-
}
|
|
70
|
-
return result
|
|
71
|
-
}, [cmds])
|
|
72
|
-
|
|
73
|
-
// Find the row index of the cursor to drive the sliding window.
|
|
74
|
-
const cursorRow = rows.findIndex(r => r.type === "cmd" && r.flat === cursor)
|
|
75
|
-
const start = Math.max(0, Math.min(cursorRow - 2, rows.length - MAX_VISIBLE))
|
|
76
|
-
const visible = rows.slice(start, start + MAX_VISIBLE)
|
|
77
|
-
const clipped = rows.length > MAX_VISIBLE
|
|
78
|
-
const above = clipped && start > 0
|
|
79
|
-
const below = clipped && start + MAX_VISIBLE < rows.length
|
|
80
|
-
const height = visible.length + 2 + (above ? 1 : 0) + (below ? 1 : 0)
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<box
|
|
84
|
-
flexDirection="column"
|
|
85
|
-
border
|
|
86
|
-
borderStyle="single"
|
|
87
|
-
borderColor={theme.border}
|
|
88
|
-
backgroundColor={theme.backgroundPanel}
|
|
89
|
-
paddingX={1}
|
|
90
|
-
height={height}
|
|
91
|
-
>
|
|
92
|
-
{above ? (
|
|
93
|
-
<box height={1} paddingLeft={1}>
|
|
94
|
-
<text fg={theme.textMuted}>↑ more</text>
|
|
95
|
-
</box>
|
|
96
|
-
) : null}
|
|
97
|
-
{visible.map((row) => {
|
|
98
|
-
if (row.type === "header") {
|
|
99
|
-
return (
|
|
100
|
-
<box key={`h-${row.cat}`} height={1} paddingLeft={1}>
|
|
101
|
-
<text>
|
|
102
|
-
<span fg={theme.textMuted}>
|
|
103
|
-
<strong>{row.cat}</strong>
|
|
104
|
-
</span>
|
|
105
|
-
</text>
|
|
106
|
-
</box>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const active = row.flat === cursor
|
|
111
|
-
const color = badge(row.cmd.source, theme)
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<box
|
|
115
|
-
key={`c-${row.cmd.name}`}
|
|
116
|
-
height={1}
|
|
117
|
-
flexDirection="row"
|
|
118
|
-
backgroundColor={active ? theme.backgroundElement : undefined}
|
|
119
|
-
onMouseOver={() => onCursor(row.flat)}
|
|
120
|
-
onMouseDown={() => onSelect(row.cmd)}
|
|
121
|
-
paddingLeft={2}
|
|
122
|
-
paddingRight={1}
|
|
123
|
-
>
|
|
124
|
-
{/* Left: /name [args] description */}
|
|
125
|
-
<box flexGrow={1} height={1}>
|
|
126
|
-
<text>
|
|
127
|
-
<span fg={active ? theme.primary : theme.text}>/{row.cmd.name}</span>
|
|
128
|
-
{row.cmd.argsHint ? (
|
|
129
|
-
<span fg={theme.textMuted}> {row.cmd.argsHint}</span>
|
|
130
|
-
) : null}
|
|
131
|
-
<span fg={theme.textMuted}> {row.cmd.description}</span>
|
|
132
|
-
</text>
|
|
133
|
-
</box>
|
|
134
|
-
|
|
135
|
-
{/* Right: source badge + keybind */}
|
|
136
|
-
<box height={1} flexDirection="row">
|
|
137
|
-
{color ? (
|
|
138
|
-
<text>
|
|
139
|
-
<span fg={color}> {row.cmd.source}</span>
|
|
140
|
-
</text>
|
|
141
|
-
) : null}
|
|
142
|
-
{row.cmd.keybind ? (
|
|
143
|
-
<text>
|
|
144
|
-
<span fg={theme.textMuted}> {row.cmd.keybind}</span>
|
|
145
|
-
</text>
|
|
146
|
-
) : null}
|
|
147
|
-
</box>
|
|
148
|
-
</box>
|
|
149
|
-
)
|
|
150
|
-
})}
|
|
151
|
-
{below ? (
|
|
152
|
-
<box height={1} paddingLeft={1}>
|
|
153
|
-
<text fg={theme.textMuted}>↓ more</text>
|
|
154
|
-
</box>
|
|
155
|
-
) : null}
|
|
156
|
-
</box>
|
|
157
|
-
)
|
|
158
|
-
})
|