herm-tui 1.0.0-dev.1 → 1.0.0-dev.2

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.
Files changed (187) hide show
  1. package/highlights-eq9cgrbb.scm +604 -0
  2. package/highlights-ghv9g403.scm +205 -0
  3. package/highlights-hk7bwhj4.scm +284 -0
  4. package/highlights-r812a2qc.scm +150 -0
  5. package/highlights-x6tmsnaa.scm +115 -0
  6. package/index.js +10375 -0
  7. package/injections-73j83es3.scm +27 -0
  8. package/package.json +14 -64
  9. package/parser.worker.js +8 -0
  10. package/tree-sitter-3jzf13jk.wasm +0 -0
  11. package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  12. package/tree-sitter-markdown-411r6y9b.wasm +0 -0
  13. package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  14. package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  15. package/tree-sitter-zig-e78zbjpm.wasm +0 -0
  16. package/scripts/postinstall.ts +0 -29
  17. package/src/app/gateway.tsx +0 -83
  18. package/src/app/gatewayEvents.ts +0 -203
  19. package/src/app/launch.ts +0 -41
  20. package/src/app/skin.tsx +0 -31
  21. package/src/app/spawnHistory.ts +0 -75
  22. package/src/app/tabs.ts +0 -23
  23. package/src/app/turnReducer.ts +0 -390
  24. package/src/app/useAppKeys.ts +0 -268
  25. package/src/app/useAtRefPopover.ts +0 -99
  26. package/src/app/useInputHistory.ts +0 -66
  27. package/src/app/useSession.ts +0 -102
  28. package/src/app/useSlashCommands.ts +0 -70
  29. package/src/app/useSlashPopover.ts +0 -48
  30. package/src/app.tsx +0 -917
  31. package/src/commands/slash.ts +0 -151
  32. package/src/components/avatar/AnimatedAvatar.tsx +0 -66
  33. package/src/components/avatar/eikon.ts +0 -144
  34. package/src/components/avatar/states/error.ts +0 -1155
  35. package/src/components/avatar/states/idle.ts +0 -1155
  36. package/src/components/avatar/states/index.ts +0 -30
  37. package/src/components/avatar/states/listening.ts +0 -1155
  38. package/src/components/avatar/states/speaking.ts +0 -1155
  39. package/src/components/avatar/states/thinking.ts +0 -1155
  40. package/src/components/avatar/states/working.ts +0 -1155
  41. package/src/components/chat/AtRefPopover.tsx +0 -54
  42. package/src/components/chat/CodeBlock.tsx +0 -67
  43. package/src/components/chat/Composer.tsx +0 -347
  44. package/src/components/chat/DiffBlock.tsx +0 -116
  45. package/src/components/chat/ErrorBlock.tsx +0 -70
  46. package/src/components/chat/MediaChip.tsx +0 -114
  47. package/src/components/chat/MessageItem.tsx +0 -282
  48. package/src/components/chat/MessageList.tsx +0 -114
  49. package/src/components/chat/PromptCard.tsx +0 -359
  50. package/src/components/chat/SlashPopover.tsx +0 -158
  51. package/src/components/chat/ThoughtCloud.tsx +0 -185
  52. package/src/components/chat/TypingIndicator.tsx +0 -25
  53. package/src/components/chat/tool/Subagent.tsx +0 -75
  54. package/src/components/chat/tool/frame.tsx +0 -69
  55. package/src/components/chat/tool/index.tsx +0 -65
  56. package/src/components/chat/tool/preview.ts +0 -57
  57. package/src/components/sidebar/ContextGauge.tsx +0 -102
  58. package/src/components/sidebar/Sidebar.tsx +0 -143
  59. package/src/components/tabs/TabBar.tsx +0 -50
  60. package/src/components/ui/FileLink.tsx +0 -52
  61. package/src/config/index.ts +0 -156
  62. package/src/config/lane.ts +0 -161
  63. package/src/config/models.ts +0 -95
  64. package/src/config/rules.ts +0 -80
  65. package/src/config/schema.ts +0 -308
  66. package/src/dialogs/alert.tsx +0 -52
  67. package/src/dialogs/chafa.tsx +0 -72
  68. package/src/dialogs/confirm.tsx +0 -58
  69. package/src/dialogs/curator.tsx +0 -153
  70. package/src/dialogs/eikon-picker.tsx +0 -95
  71. package/src/dialogs/help.tsx +0 -80
  72. package/src/dialogs/history.tsx +0 -92
  73. package/src/dialogs/info.tsx +0 -115
  74. package/src/dialogs/keys.tsx +0 -170
  75. package/src/dialogs/logs.tsx +0 -42
  76. package/src/dialogs/message.tsx +0 -38
  77. package/src/dialogs/model-picker.tsx +0 -123
  78. package/src/dialogs/new-profile.tsx +0 -69
  79. package/src/dialogs/new-task.tsx +0 -103
  80. package/src/dialogs/profile.tsx +0 -55
  81. package/src/dialogs/rollback.tsx +0 -190
  82. package/src/dialogs/spawn-history.tsx +0 -80
  83. package/src/dialogs/text-prompt.tsx +0 -68
  84. package/src/dialogs/theme-picker.tsx +0 -50
  85. package/src/home/index.ts +0 -23
  86. package/src/home/store.ts +0 -267
  87. package/src/index.tsx +0 -113
  88. package/src/keys/catalog.ts +0 -115
  89. package/src/keys/chord.ts +0 -125
  90. package/src/keys/conflicts.ts +0 -48
  91. package/src/keys/context.tsx +0 -112
  92. package/src/keys/index.ts +0 -5
  93. package/src/keys/list.ts +0 -94
  94. package/src/keys/oc-compat.ts +0 -87
  95. package/src/tabs/Agents.tsx +0 -607
  96. package/src/tabs/Analytics.tsx +0 -154
  97. package/src/tabs/Chat.tsx +0 -50
  98. package/src/tabs/Config.tsx +0 -605
  99. package/src/tabs/Context.tsx +0 -599
  100. package/src/tabs/Cron.tsx +0 -294
  101. package/src/tabs/Env.tsx +0 -227
  102. package/src/tabs/Kanban.tsx +0 -367
  103. package/src/tabs/Memory.tsx +0 -294
  104. package/src/tabs/Sessions.tsx +0 -786
  105. package/src/tabs/Skills.tsx +0 -507
  106. package/src/tabs/Toolsets.tsx +0 -266
  107. package/src/theme/builtin.ts +0 -78
  108. package/src/theme/context.tsx +0 -106
  109. package/src/theme/index.ts +0 -4
  110. package/src/theme/resolve.ts +0 -134
  111. package/src/theme/syntax.ts +0 -31
  112. package/src/theme/themes/aura.json +0 -69
  113. package/src/theme/themes/ayu.json +0 -80
  114. package/src/theme/themes/carbonfox.json +0 -248
  115. package/src/theme/themes/catppuccin-frappe.json +0 -233
  116. package/src/theme/themes/catppuccin-macchiato.json +0 -233
  117. package/src/theme/themes/catppuccin.json +0 -112
  118. package/src/theme/themes/cobalt2.json +0 -228
  119. package/src/theme/themes/cursor.json +0 -249
  120. package/src/theme/themes/dracula.json +0 -219
  121. package/src/theme/themes/everforest.json +0 -241
  122. package/src/theme/themes/flexoki.json +0 -237
  123. package/src/theme/themes/github.json +0 -233
  124. package/src/theme/themes/gruvbox.json +0 -242
  125. package/src/theme/themes/kanagawa.json +0 -77
  126. package/src/theme/themes/lucent-orng.json +0 -237
  127. package/src/theme/themes/material.json +0 -235
  128. package/src/theme/themes/matrix.json +0 -77
  129. package/src/theme/themes/mercury.json +0 -252
  130. package/src/theme/themes/monokai.json +0 -221
  131. package/src/theme/themes/nightowl.json +0 -221
  132. package/src/theme/themes/nord.json +0 -223
  133. package/src/theme/themes/one-dark.json +0 -84
  134. package/src/theme/themes/opencode.json +0 -245
  135. package/src/theme/themes/orng.json +0 -249
  136. package/src/theme/themes/osaka-jade.json +0 -93
  137. package/src/theme/themes/palenight.json +0 -222
  138. package/src/theme/themes/rosepine.json +0 -234
  139. package/src/theme/themes/solarized.json +0 -223
  140. package/src/theme/themes/synthwave84.json +0 -226
  141. package/src/theme/themes/tokyonight.json +0 -243
  142. package/src/theme/themes/vercel.json +0 -245
  143. package/src/theme/themes/vesper.json +0 -218
  144. package/src/theme/themes/zenburn.json +0 -223
  145. package/src/theme/types.ts +0 -119
  146. package/src/types/message.ts +0 -97
  147. package/src/ui/ChafaImage.tsx +0 -64
  148. package/src/ui/Splash.tsx +0 -118
  149. package/src/ui/borders.ts +0 -28
  150. package/src/ui/command.tsx +0 -104
  151. package/src/ui/dialog-select.tsx +0 -164
  152. package/src/ui/dialog.tsx +0 -102
  153. package/src/ui/fmt.ts +0 -82
  154. package/src/ui/kv.tsx +0 -28
  155. package/src/ui/shell.tsx +0 -45
  156. package/src/ui/spinner.tsx +0 -59
  157. package/src/ui/splash-art.ts +0 -123
  158. package/src/ui/table.tsx +0 -117
  159. package/src/ui/ticker.tsx +0 -90
  160. package/src/ui/toast.tsx +0 -130
  161. package/src/utils/categorical.ts +0 -77
  162. package/src/utils/chafa.ts +0 -173
  163. package/src/utils/clipboard.ts +0 -67
  164. package/src/utils/context-segments.ts +0 -317
  165. package/src/utils/control.ts +0 -495
  166. package/src/utils/drop.ts +0 -25
  167. package/src/utils/editor.ts +0 -33
  168. package/src/utils/fuzzy.ts +0 -45
  169. package/src/utils/gateway-client.ts +0 -253
  170. package/src/utils/gateway-types.ts +0 -282
  171. package/src/utils/git.ts +0 -57
  172. package/src/utils/hermes-analytics.ts +0 -134
  173. package/src/utils/hermes-home.ts +0 -821
  174. package/src/utils/hermes-kanban.ts +0 -154
  175. package/src/utils/hermes-profiles.ts +0 -217
  176. package/src/utils/interpolate.ts +0 -31
  177. package/src/utils/math-unicode.ts +0 -818
  178. package/src/utils/memory-activity.ts +0 -140
  179. package/src/utils/open-file.ts +0 -13
  180. package/src/utils/paths.ts +0 -52
  181. package/src/utils/perf.ts +0 -235
  182. package/src/utils/preferences.ts +0 -150
  183. package/src/utils/sessions-db.ts +0 -396
  184. package/src/utils/subagent-tree.ts +0 -146
  185. package/src/utils/terminal-reset.ts +0 -129
  186. package/src/utils/tips.ts +0 -67
  187. 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
- })