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