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,495 +0,0 @@
1
- /**
2
- * control.ts — HTTP control server for headless/automated interaction.
3
- *
4
- * Runs on CONTROL_PORT (default 7777) when CONTROL=1 env is set.
5
- * Exposes imperative actions: tab navigation, message sending, perf dumps,
6
- * key injection, and DOM queries for automated testing.
7
- *
8
- * Usage:
9
- * CONTROL=1 bun run dev # start with control server
10
- * curl localhost:7777/status # get app state
11
- * curl localhost:7777/tab/3 # switch to Sessions tab
12
- * curl -X POST localhost:7777/send -d '{"message":"hello"}'
13
- * curl -X POST localhost:7777/key -d '{"name":"tab"}'
14
- * curl localhost:7777/focus # get focus tree
15
- * curl localhost:7777/perf # dump perf report
16
- *
17
- * The bridge is set by AppInner via control.setBridge({...}).
18
- *
19
- * SAFETY: Key injection is blocked for keys that would mutate state
20
- * on dangerous tabs (Config, Sessions) unless safe=false is passed.
21
- */
22
-
23
- import * as perf from "./perf"
24
- import { TABS, TAB_MAX, CHAT_TAB } from "../app/tabs"
25
-
26
- const PORT = Number(process.env.CONTROL_PORT) || 7777
27
- export const enabled = process.env.CONTROL === "1"
28
-
29
- const TAB_NAMES: readonly string[] = TABS.map(t => t.name)
30
-
31
- type Bridge = {
32
- tab: () => number
33
- setTab: (n: number) => void
34
- send: (msg: string) => void
35
- ready: () => boolean
36
- streaming: () => boolean
37
- messages: () => number
38
- session: () => string
39
- input: () => string
40
- setInput: (v: string) => void
41
- focusRegion: () => "input" | "content"
42
- setFocusRegion: (r: "input" | "content") => void
43
- renderer: () => unknown // OpenTUI renderer instance
44
- logs: (n?: number) => string
45
- }
46
-
47
- let bridge: Bridge | null = null
48
- let pendingTab: number | null = null
49
-
50
- export function setBridge(b: Bridge) {
51
- bridge = b
52
- }
53
-
54
- function currentTab(): number {
55
- if (pendingTab !== null) return pendingTab
56
- return bridge?.tab() ?? 0
57
- }
58
-
59
- const json = (data: unknown, status = 200) =>
60
- new Response(JSON.stringify(data), {
61
- status,
62
- headers: { "Content-Type": "application/json" },
63
- })
64
-
65
- // Keys that can mutate state on specific tabs
66
- const DANGEROUS_KEYS: Record<number, Set<string>> = {
67
- 1: new Set(["return"]), // Chat: Enter sends message
68
- 3: new Set(["d", "delete", "return"]), // Sessions: d=delete, Enter=switch session
69
- 8: new Set(["space", "return", "h", "l", "]", "[", "ctrl+s"]), // Config: toggles, edits, save
70
- 9: new Set(["return", "space", "d", "delete"]), // Env: potential mutations
71
- }
72
-
73
- function isDangerous(tab: number, keyName: string, ctrl: boolean): boolean {
74
- const set = DANGEROUS_KEYS[tab]
75
- if (!set) return false
76
- const id = ctrl ? `ctrl+${keyName}` : keyName
77
- return set.has(id)
78
- }
79
-
80
- // ─── Key injection ───────────────────────────────────────────────────
81
-
82
- interface ParsedKey {
83
- name: string
84
- ctrl: boolean
85
- meta: boolean
86
- shift: boolean
87
- option: boolean
88
- sequence: string
89
- number: boolean
90
- raw: string
91
- eventType: "press" | "release"
92
- source: "raw" | "kitty"
93
- repeated?: boolean
94
- }
95
-
96
- function makeKey(opts: {
97
- name: string
98
- ctrl?: boolean
99
- shift?: boolean
100
- meta?: boolean
101
- raw?: string
102
- }): ParsedKey {
103
- return {
104
- name: opts.name,
105
- ctrl: opts.ctrl ?? false,
106
- meta: opts.meta ?? false,
107
- shift: opts.shift ?? false,
108
- option: false,
109
- sequence: opts.raw ?? opts.name,
110
- number: false,
111
- raw: opts.raw ?? opts.name,
112
- eventType: "press",
113
- source: "raw",
114
- }
115
- }
116
-
117
- function injectKey(renderer: unknown, key: ParsedKey): boolean {
118
- const r = renderer as { keyInput?: { processParsedKey?: (k: ParsedKey) => boolean } }
119
- if (!r?.keyInput?.processParsedKey) return false
120
- return r.keyInput.processParsedKey(key)
121
- }
122
-
123
- // ─── Focus tree query ────────────────────────────────────────────────
124
-
125
- interface FocusNode {
126
- type: string
127
- focused: boolean
128
- focusable: boolean
129
- children: FocusNode[]
130
- text?: string
131
- }
132
-
133
- type AnyNode = {
134
- constructor?: { name?: string }
135
- focused?: boolean
136
- focusable?: boolean
137
- getChildren?: () => AnyNode[]
138
- getChildrenCount?: () => number
139
- _childrenInLayoutOrder?: AnyNode[]
140
- textContent?: string
141
- text?: string
142
- value?: string
143
- id?: string
144
- _type?: string
145
- tagName?: string
146
- }
147
-
148
- function getNodeChildren(n: AnyNode): AnyNode[] {
149
- if (n.getChildren) return n.getChildren()
150
- if (n._childrenInLayoutOrder) return [...n._childrenInLayoutOrder]
151
- return []
152
- }
153
-
154
- function getNodeType(n: AnyNode): string {
155
- return n._type || n.tagName || n.constructor?.name || "unknown"
156
- }
157
-
158
- function buildFocusTree(node: unknown, depth = 0): FocusNode | null {
159
- if (!node || typeof node !== "object") return null
160
- const n = node as AnyNode
161
-
162
- const type = getNodeType(n)
163
- const focused = n.focused ?? false
164
- const focusable = n.focusable ?? false
165
- const children: FocusNode[] = []
166
-
167
- if (depth < 20) {
168
- for (const child of getNodeChildren(n)) {
169
- const c = buildFocusTree(child, depth + 1)
170
- if (c) children.push(c)
171
- }
172
- }
173
-
174
- // Skip non-focusable nodes with no focusable descendants
175
- const hasFocusable = focusable || children.some(c =>
176
- c.focusable || c.focused || c.children.length > 0
177
- )
178
- if (!hasFocusable && !focused && depth > 0) return null
179
-
180
- const text = (n.value || n.textContent || n.text || undefined) as string | undefined
181
-
182
- return { type, focused, focusable, children, text }
183
- }
184
-
185
- function findFocused(node: unknown): string | null {
186
- if (!node || typeof node !== "object") return null
187
- const n = node as AnyNode
188
- if (n.focused) return getNodeType(n)
189
- for (const child of getNodeChildren(n)) {
190
- const found = findFocused(child)
191
- if (found) return found
192
- }
193
- return null
194
- }
195
-
196
- function countNodes(node: unknown): { total: number; focusable: number; focused: number } {
197
- const result = { total: 0, focusable: 0, focused: 0 }
198
- function walk(n: unknown) {
199
- if (!n || typeof n !== "object") return
200
- const nd = n as AnyNode
201
- result.total++
202
- if (nd.focusable) result.focusable++
203
- if (nd.focused) result.focused++
204
- for (const child of getNodeChildren(nd)) walk(child)
205
- }
206
- walk(node)
207
- return result
208
- }
209
-
210
- // ─── Request handler ─────────────────────────────────────────────────
211
-
212
- async function handle(req: Request): Promise<Response> {
213
- const url = new URL(req.url)
214
- const path = url.pathname
215
-
216
- if (!bridge) return json({ error: "bridge not ready" }, 503)
217
-
218
- // GET /status — app state snapshot
219
- if (path === "/status") {
220
- const m = process.memoryUsage()
221
- const tab = currentTab()
222
- // Clear pending tab once React has had time to commit
223
- pendingTab = null
224
- return json({
225
- tab,
226
- tabName: TAB_NAMES[tab] ?? "unknown",
227
- ready: bridge.ready(),
228
- streaming: bridge.streaming(),
229
- messages: bridge.messages(),
230
- session: bridge.session(),
231
- input: bridge.input(),
232
- focusRegion: bridge.focusRegion(),
233
- rss: Math.round(m.rss / 1024 / 1024),
234
- heap: Math.round(m.heapUsed / 1024 / 1024),
235
- })
236
- }
237
-
238
- // GET /tab/:n — switch tab by injecting Ctrl+Right/Left key events
239
- const tabMatch = path.match(/^\/tab\/(\d+)$/)
240
- if (tabMatch) {
241
- const n = Number(tabMatch[1])
242
- if (n < 0 || n > TAB_MAX) return json({ error: `tab 0-${TAB_MAX}` }, 400)
243
-
244
- const renderer = bridge.renderer()
245
- if (renderer) {
246
- // Inject Ctrl+Left/Right keys to navigate to target tab
247
- const cur = bridge.tab()
248
- const diff = n - cur
249
- if (diff !== 0) {
250
- const keyName = diff > 0 ? "right" : "left"
251
- const steps = Math.abs(diff)
252
- for (let i = 0; i < steps; i++) {
253
- injectKey(renderer, makeKey({ name: keyName, ctrl: true }))
254
- }
255
- }
256
- } else {
257
- // Fallback to direct setState (may not work reliably)
258
- bridge.setTab(n)
259
- }
260
- pendingTab = n
261
- return json({ tab: n, tabName: TAB_NAMES[n] })
262
- }
263
-
264
- // POST /send — send a message
265
- if (path === "/send" && req.method === "POST") {
266
- const body = await req.json() as { message?: string }
267
- if (!body.message) return json({ error: "message required" }, 400)
268
- if (!bridge.ready()) return json({ error: "not connected" }, 503)
269
- if (bridge.streaming()) return json({ error: "already streaming" }, 409)
270
- bridge.send(body.message)
271
- return json({ sent: true, message: body.message })
272
- }
273
-
274
- // POST /key — inject a key event
275
- //
276
- // Body: { name: "tab", ctrl?: bool, shift?: bool, meta?: bool, raw?: string, safe?: bool }
277
- //
278
- // safe (default true): blocks keys known to mutate state on current tab.
279
- // Set safe=false to override (use for intentional mutation testing).
280
- if (path === "/key" && req.method === "POST") {
281
- const body = await req.json() as {
282
- name?: string
283
- ctrl?: boolean
284
- shift?: boolean
285
- meta?: boolean
286
- raw?: string
287
- safe?: boolean
288
- }
289
- if (!body.name) return json({ error: "name required" }, 400)
290
-
291
- const renderer = bridge.renderer()
292
- if (!renderer) return json({ error: "renderer not available" }, 503)
293
-
294
- const safe = body.safe !== false // default true
295
- const tab = currentTab()
296
-
297
- if (safe && isDangerous(tab, body.name, !!body.ctrl)) {
298
- return json({
299
- error: "blocked",
300
- reason: `Key "${body.ctrl ? "ctrl+" : ""}${body.name}" is dangerous on tab ${TAB_NAMES[tab]} (index ${tab}). Pass safe=false to override.`,
301
- tab,
302
- tabName: TAB_NAMES[tab],
303
- }, 403)
304
- }
305
-
306
- const key = makeKey({
307
- name: body.name,
308
- ctrl: body.ctrl,
309
- shift: body.shift,
310
- meta: body.meta,
311
- raw: body.raw ?? (body.name.length === 1 ? body.name : ""),
312
- })
313
-
314
- const handled = injectKey(renderer, key)
315
- return json({ injected: true, handled, key: body.name, tab, tabName: TAB_NAMES[tab] })
316
- }
317
-
318
- // POST /keys — inject a sequence of key events
319
- //
320
- // Body: { keys: [{name, ctrl?, ...}, ...], delay?: number, safe?: bool }
321
- if (path === "/keys" && req.method === "POST") {
322
- const body = await req.json() as {
323
- keys?: Array<{ name: string; ctrl?: boolean; shift?: boolean; meta?: boolean; raw?: string }>
324
- delay?: number
325
- safe?: boolean
326
- }
327
- if (!body.keys?.length) return json({ error: "keys array required" }, 400)
328
-
329
- const renderer = bridge.renderer()
330
- if (!renderer) return json({ error: "renderer not available" }, 503)
331
-
332
- const safe = body.safe !== false
333
- const tab = currentTab()
334
- const delay = body.delay ?? 0
335
- const results: Array<{ key: string; injected: boolean; handled: boolean; blocked?: boolean }> = []
336
-
337
- for (const k of body.keys) {
338
- if (safe && isDangerous(currentTab(), k.name, !!k.ctrl)) {
339
- results.push({ key: k.name, injected: false, handled: false, blocked: true })
340
- continue
341
- }
342
- const key = makeKey({
343
- name: k.name,
344
- ctrl: k.ctrl,
345
- shift: k.shift,
346
- meta: k.meta,
347
- raw: k.raw ?? (k.name.length === 1 ? k.name : ""),
348
- })
349
- const handled = injectKey(renderer, key)
350
- results.push({ key: k.name, injected: true, handled })
351
- if (delay > 0) await new Promise(r => setTimeout(r, delay))
352
- }
353
-
354
- return json({ results, tab, tabName: TAB_NAMES[tab] })
355
- }
356
-
357
- // POST /type — inject a string as individual keystrokes
358
- //
359
- // Body: { text: "hello", safe?: bool, delay?: ms }
360
- // delay paces characters for cinematic typing (demo recordings).
361
- if (path === "/type" && req.method === "POST") {
362
- const body = await req.json() as { text?: string; safe?: boolean; delay?: number }
363
- if (!body.text) return json({ error: "text required" }, 400)
364
-
365
- const renderer = bridge.renderer()
366
- if (!renderer) return json({ error: "renderer not available" }, 503)
367
-
368
- const safe = body.safe !== false
369
- const tab = currentTab()
370
- const delay = body.delay ?? 0
371
- let count = 0
372
-
373
- for (const ch of body.text) {
374
- if (safe && isDangerous(tab, ch, false)) continue
375
- const key = makeKey({ name: ch, raw: ch })
376
- injectKey(renderer, key)
377
- count++
378
- if (delay > 0) await new Promise(r => setTimeout(r, delay))
379
- }
380
-
381
- return json({ typed: count, total: body.text.length, tab, tabName: TAB_NAMES[tab] })
382
- }
383
-
384
- // POST /input — set composer value in one shot (no per-char keys).
385
- if (path === "/input" && req.method === "POST") {
386
- const body = await req.json() as { text?: string }
387
- bridge.setInput(body.text ?? "")
388
- return json({ ok: true, text: body.text ?? "" })
389
- }
390
-
391
- // GET /quit — clean exit so a recording PTY sees EOF. Macrotask so the
392
- // 200 flushes before the process dies.
393
- if (path === "/quit") {
394
- setTimeout(() => process.exit(0), 10)
395
- return json({ ok: true })
396
- }
397
-
398
- // GET /focus — focus tree (focusable elements and their state)
399
- if (path === "/focus") {
400
- const r = bridge.renderer() as {
401
- root?: unknown
402
- currentFocusedRenderable?: AnyNode | null
403
- } | null
404
- if (!r?.root) return json({ error: "no renderer root" }, 503)
405
- const counts = countNodes(r.root)
406
- const tree = buildFocusTree(r.root)
407
- const focused = findFocused(r.root)
408
- const currentFocus = r.currentFocusedRenderable
409
- ? getNodeType(r.currentFocusedRenderable)
410
- : null
411
- return json({ focused, currentFocus, counts, tree })
412
- }
413
-
414
- // GET /frame — current screen buffer as plain text. `?grep=pat` returns
415
- // only matching lines. `?json=1` wraps in {frame, match, lines}.
416
- if (path === "/frame") {
417
- const r = bridge.renderer() as {
418
- currentRenderBuffer?: { getRealCharBytes(nl: boolean): Uint8Array }
419
- } | null
420
- if (!r?.currentRenderBuffer) return json({ error: "no render buffer" }, 503)
421
- const frame = new TextDecoder().decode(r.currentRenderBuffer.getRealCharBytes(true))
422
- const grep = url.searchParams.get("grep")
423
- const body = grep ? frame.split("\n").filter(l => l.includes(grep)).join("\n") : frame
424
- if (url.searchParams.get("json") === "1") {
425
- return json({
426
- frame: body,
427
- match: grep ? frame.includes(grep) : undefined,
428
- lines: frame.split("\n").length,
429
- })
430
- }
431
- return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } })
432
- }
433
-
434
- // GET /logs?n=N — gateway stderr ring buffer (same source as /logs dialog)
435
- if (path === "/logs") {
436
- const n = Number(url.searchParams.get("n")) || 200
437
- return new Response(bridge.logs(n), { headers: { "Content-Type": "text/plain; charset=utf-8" } })
438
- }
439
-
440
- // GET /perf — return all profiling data as JSON
441
- if (path === "/perf") {
442
- const d = perf.data()
443
- if (!d) return json({ error: "PERF not enabled" }, 400)
444
- return json(d)
445
- }
446
-
447
- // GET /tabs — cycle through all tabs with a delay
448
- if (path === "/tabs") {
449
- const ms = Number(url.searchParams.get("delay") || "500")
450
- for (let i = 0; i <= TAB_MAX; i++) {
451
- bridge.setTab(i)
452
- await new Promise(r => setTimeout(r, ms))
453
- }
454
- bridge.setTab(CHAT_TAB)
455
- return json({ cycled: TAB_MAX + 1, delay: ms })
456
- }
457
-
458
- // GET /mem — memory snapshot
459
- if (path === "/mem") {
460
- perf.mem("control:snapshot")
461
- const m = process.memoryUsage()
462
- return json({
463
- rss: Math.round(m.rss / 1024 / 1024),
464
- heap: Math.round(m.heapUsed / 1024 / 1024),
465
- heapTotal: Math.round(m.heapTotal / 1024 / 1024),
466
- external: Math.round(m.external / 1024 / 1024),
467
- })
468
- }
469
-
470
- return json({
471
- error: "not found",
472
- routes: [
473
- "GET /status",
474
- "GET /tab/:n",
475
- "POST /send {message}",
476
- "POST /key {name, ctrl?, shift?, meta?, raw?, safe?}",
477
- "POST /keys {keys: [{name, ...}], delay?, safe?}",
478
- "POST /type {text, delay?, safe?}",
479
- "POST /input {text}",
480
- "GET /quit",
481
- "GET /frame ?grep=pat&json=1",
482
- "GET /logs ?n=200",
483
- "GET /focus",
484
- "GET /perf",
485
- "GET /tabs",
486
- "GET /mem",
487
- ],
488
- }, 404)
489
- }
490
-
491
- export function start() {
492
- if (!enabled) return
493
- Bun.serve({ port: PORT, fetch: handle })
494
- process.stderr.write(`\x1b[90m[control] http://localhost:${PORT}\x1b[0m\n`)
495
- }
package/src/utils/drop.ts DELETED
@@ -1,25 +0,0 @@
1
- // Cheap client-side sniff for "this paste is probably a local file path".
2
- // Mirrors the starts_like_path gate in hermes cli._detect_file_drop — the
3
- // gateway's input.detect_drop RPC is the authority (it stats the file and
4
- // handles quoting/escapes/file://); this only decides whether to bother
5
- // asking. Kept deliberately narrow so prose that happens to start with `/`
6
- // (e.g. a pasted regex) still falls through to verbatim insert on miss.
7
-
8
- /** Windows drive prefix (`C:\` or `C:/`), optionally behind a quote. */
9
- const winDrive = (s: string, off = 0) =>
10
- s.length >= off + 3 && /[A-Za-z]/.test(s[off]!) && s[off + 1] === ":" && (s[off + 2] === "\\" || s[off + 2] === "/")
11
-
12
- export function looksLikePath(s: string): boolean {
13
- const t = s.trim()
14
- if (!t || t.includes("\n")) return false
15
- if (t.startsWith("file://")) return true
16
- if (t.startsWith("/") || t.startsWith("~") || t.startsWith("./") || t.startsWith("../")) return true
17
- if (winDrive(t)) return true
18
- const q = t[0]
19
- if (q === '"' || q === "'") {
20
- const inner = t[1]
21
- if (inner === "/" || inner === "~") return true
22
- if (winDrive(t, 1)) return true
23
- }
24
- return false
25
- }
@@ -1,33 +0,0 @@
1
- // Suspend the renderer, open $VISUAL/$EDITOR on a tmpfile seeded with the
2
- // current input, read it back. Returns undefined if no editor configured
3
- // or the user emptied the file.
4
-
5
- import { tmpdir } from "node:os"
6
- import { join } from "node:path"
7
- import { rm } from "node:fs/promises"
8
- import type { CliRenderer } from "@opentui/core"
9
-
10
- export async function editInEditor(renderer: CliRenderer, seed: string): Promise<string | undefined> {
11
- const cmd = process.env.VISUAL || process.env.EDITOR
12
- if (!cmd) return undefined
13
-
14
- const path = join(tmpdir(), `herm-${Date.now()}.md`)
15
- await Bun.write(path, seed)
16
-
17
- renderer.suspend()
18
- renderer.currentRenderBuffer.clear()
19
- try {
20
- const parts = cmd.split(" ")
21
- const proc = Bun.spawn([...parts, path], {
22
- stdin: "inherit", stdout: "inherit", stderr: "inherit",
23
- })
24
- await proc.exited
25
- const text = await Bun.file(path).text().catch(() => "")
26
- return text.trim() || undefined
27
- } finally {
28
- rm(path, { force: true }).catch(() => {})
29
- renderer.currentRenderBuffer.clear()
30
- renderer.resume()
31
- renderer.requestRender()
32
- }
33
- }
@@ -1,45 +0,0 @@
1
- /**
2
- * Fuzzy subsequence scorer.
3
- *
4
- * Returns a positive number when `needle` is a (case-insensitive) subsequence
5
- * of `hay`, weighted so that tighter / earlier / boundary-aligned matches rank
6
- * higher. Returns 0 when the needle cannot be found as a subsequence.
7
- *
8
- * Bonuses:
9
- * - start of string
10
- * - consecutive characters
11
- * - word boundaries: after `_ - / . space` or a lower→Upper camel hump
12
- * - exact prefix (guarantees prefix matches outrank scattered ones)
13
- */
14
-
15
- const SEP = new Set(["-", "_", "/", " ", "."])
16
-
17
- function boundary(hay: string, i: number) {
18
- if (i === 0) return true
19
- const prev = hay[i - 1]
20
- if (SEP.has(prev)) return true
21
- if (prev === prev.toLowerCase() && hay[i] !== hay[i].toLowerCase()) return true
22
- return false
23
- }
24
-
25
- export function score(needle: string, hay: string): number {
26
- if (!needle) return 0
27
- const n = needle.toLowerCase()
28
- const h = hay.toLowerCase()
29
- let pts = 0
30
- let from = 0
31
- let prev = -2
32
- for (let i = 0; i < n.length; i++) {
33
- const at = h.indexOf(n[i], from)
34
- if (at < 0) return 0
35
- pts += 1
36
- if (at === 0) pts += 8
37
- if (at === prev + 1) pts += 5
38
- if (at !== prev + 1 && boundary(hay, at)) pts += 4
39
- pts -= (at - (prev < 0 ? 0 : prev + 1)) * 0.1
40
- prev = at
41
- from = at + 1
42
- }
43
- if (h.startsWith(n)) pts += 100
44
- return pts - hay.length * 0.01
45
- }