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

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 (192) hide show
  1. package/README.md +8 -4
  2. package/assets/eikons/ares.eikon +367 -0
  3. package/assets/eikons/default.eikon +398 -0
  4. package/assets/eikons/mono.eikon +395 -0
  5. package/db.worker.js +81 -0
  6. package/highlights-eq9cgrbb.scm +604 -0
  7. package/highlights-ghv9g403.scm +205 -0
  8. package/highlights-hk7bwhj4.scm +284 -0
  9. package/highlights-r812a2qc.scm +150 -0
  10. package/highlights-x6tmsnaa.scm +115 -0
  11. package/index.js +4151 -0
  12. package/injections-73j83es3.scm +27 -0
  13. package/package.json +14 -64
  14. package/parser.worker.js +8 -0
  15. package/tree-sitter-3jzf13jk.wasm +0 -0
  16. package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  17. package/tree-sitter-markdown-411r6y9b.wasm +0 -0
  18. package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  19. package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  20. package/tree-sitter-zig-e78zbjpm.wasm +0 -0
  21. package/scripts/postinstall.ts +0 -29
  22. package/src/app/gateway.tsx +0 -83
  23. package/src/app/gatewayEvents.ts +0 -203
  24. package/src/app/launch.ts +0 -41
  25. package/src/app/skin.tsx +0 -31
  26. package/src/app/spawnHistory.ts +0 -75
  27. package/src/app/tabs.ts +0 -23
  28. package/src/app/turnReducer.ts +0 -390
  29. package/src/app/useAppKeys.ts +0 -268
  30. package/src/app/useAtRefPopover.ts +0 -99
  31. package/src/app/useInputHistory.ts +0 -66
  32. package/src/app/useSession.ts +0 -102
  33. package/src/app/useSlashCommands.ts +0 -70
  34. package/src/app/useSlashPopover.ts +0 -48
  35. package/src/app.tsx +0 -917
  36. package/src/commands/slash.ts +0 -151
  37. package/src/components/avatar/AnimatedAvatar.tsx +0 -66
  38. package/src/components/avatar/eikon.ts +0 -144
  39. package/src/components/avatar/states/error.ts +0 -1155
  40. package/src/components/avatar/states/idle.ts +0 -1155
  41. package/src/components/avatar/states/index.ts +0 -30
  42. package/src/components/avatar/states/listening.ts +0 -1155
  43. package/src/components/avatar/states/speaking.ts +0 -1155
  44. package/src/components/avatar/states/thinking.ts +0 -1155
  45. package/src/components/avatar/states/working.ts +0 -1155
  46. package/src/components/chat/AtRefPopover.tsx +0 -54
  47. package/src/components/chat/CodeBlock.tsx +0 -67
  48. package/src/components/chat/Composer.tsx +0 -347
  49. package/src/components/chat/DiffBlock.tsx +0 -116
  50. package/src/components/chat/ErrorBlock.tsx +0 -70
  51. package/src/components/chat/MediaChip.tsx +0 -114
  52. package/src/components/chat/MessageItem.tsx +0 -282
  53. package/src/components/chat/MessageList.tsx +0 -114
  54. package/src/components/chat/PromptCard.tsx +0 -359
  55. package/src/components/chat/SlashPopover.tsx +0 -158
  56. package/src/components/chat/ThoughtCloud.tsx +0 -185
  57. package/src/components/chat/TypingIndicator.tsx +0 -25
  58. package/src/components/chat/tool/Subagent.tsx +0 -75
  59. package/src/components/chat/tool/frame.tsx +0 -69
  60. package/src/components/chat/tool/index.tsx +0 -65
  61. package/src/components/chat/tool/preview.ts +0 -57
  62. package/src/components/sidebar/ContextGauge.tsx +0 -102
  63. package/src/components/sidebar/Sidebar.tsx +0 -143
  64. package/src/components/tabs/TabBar.tsx +0 -50
  65. package/src/components/ui/FileLink.tsx +0 -52
  66. package/src/config/index.ts +0 -156
  67. package/src/config/lane.ts +0 -161
  68. package/src/config/models.ts +0 -95
  69. package/src/config/rules.ts +0 -80
  70. package/src/config/schema.ts +0 -308
  71. package/src/dialogs/alert.tsx +0 -52
  72. package/src/dialogs/chafa.tsx +0 -72
  73. package/src/dialogs/confirm.tsx +0 -58
  74. package/src/dialogs/curator.tsx +0 -153
  75. package/src/dialogs/eikon-picker.tsx +0 -95
  76. package/src/dialogs/help.tsx +0 -80
  77. package/src/dialogs/history.tsx +0 -92
  78. package/src/dialogs/info.tsx +0 -115
  79. package/src/dialogs/keys.tsx +0 -170
  80. package/src/dialogs/logs.tsx +0 -42
  81. package/src/dialogs/message.tsx +0 -38
  82. package/src/dialogs/model-picker.tsx +0 -123
  83. package/src/dialogs/new-profile.tsx +0 -69
  84. package/src/dialogs/new-task.tsx +0 -103
  85. package/src/dialogs/profile.tsx +0 -55
  86. package/src/dialogs/rollback.tsx +0 -190
  87. package/src/dialogs/spawn-history.tsx +0 -80
  88. package/src/dialogs/text-prompt.tsx +0 -68
  89. package/src/dialogs/theme-picker.tsx +0 -50
  90. package/src/home/index.ts +0 -23
  91. package/src/home/store.ts +0 -267
  92. package/src/index.tsx +0 -113
  93. package/src/keys/catalog.ts +0 -115
  94. package/src/keys/chord.ts +0 -125
  95. package/src/keys/conflicts.ts +0 -48
  96. package/src/keys/context.tsx +0 -112
  97. package/src/keys/index.ts +0 -5
  98. package/src/keys/list.ts +0 -94
  99. package/src/keys/oc-compat.ts +0 -87
  100. package/src/tabs/Agents.tsx +0 -607
  101. package/src/tabs/Analytics.tsx +0 -154
  102. package/src/tabs/Chat.tsx +0 -50
  103. package/src/tabs/Config.tsx +0 -605
  104. package/src/tabs/Context.tsx +0 -599
  105. package/src/tabs/Cron.tsx +0 -294
  106. package/src/tabs/Env.tsx +0 -227
  107. package/src/tabs/Kanban.tsx +0 -367
  108. package/src/tabs/Memory.tsx +0 -294
  109. package/src/tabs/Sessions.tsx +0 -786
  110. package/src/tabs/Skills.tsx +0 -507
  111. package/src/tabs/Toolsets.tsx +0 -266
  112. package/src/theme/builtin.ts +0 -78
  113. package/src/theme/context.tsx +0 -106
  114. package/src/theme/index.ts +0 -4
  115. package/src/theme/resolve.ts +0 -134
  116. package/src/theme/syntax.ts +0 -31
  117. package/src/theme/themes/aura.json +0 -69
  118. package/src/theme/themes/ayu.json +0 -80
  119. package/src/theme/themes/carbonfox.json +0 -248
  120. package/src/theme/themes/catppuccin-frappe.json +0 -233
  121. package/src/theme/themes/catppuccin-macchiato.json +0 -233
  122. package/src/theme/themes/catppuccin.json +0 -112
  123. package/src/theme/themes/cobalt2.json +0 -228
  124. package/src/theme/themes/cursor.json +0 -249
  125. package/src/theme/themes/dracula.json +0 -219
  126. package/src/theme/themes/everforest.json +0 -241
  127. package/src/theme/themes/flexoki.json +0 -237
  128. package/src/theme/themes/github.json +0 -233
  129. package/src/theme/themes/gruvbox.json +0 -242
  130. package/src/theme/themes/kanagawa.json +0 -77
  131. package/src/theme/themes/lucent-orng.json +0 -237
  132. package/src/theme/themes/material.json +0 -235
  133. package/src/theme/themes/matrix.json +0 -77
  134. package/src/theme/themes/mercury.json +0 -252
  135. package/src/theme/themes/monokai.json +0 -221
  136. package/src/theme/themes/nightowl.json +0 -221
  137. package/src/theme/themes/nord.json +0 -223
  138. package/src/theme/themes/one-dark.json +0 -84
  139. package/src/theme/themes/opencode.json +0 -245
  140. package/src/theme/themes/orng.json +0 -249
  141. package/src/theme/themes/osaka-jade.json +0 -93
  142. package/src/theme/themes/palenight.json +0 -222
  143. package/src/theme/themes/rosepine.json +0 -234
  144. package/src/theme/themes/solarized.json +0 -223
  145. package/src/theme/themes/synthwave84.json +0 -226
  146. package/src/theme/themes/tokyonight.json +0 -243
  147. package/src/theme/themes/vercel.json +0 -245
  148. package/src/theme/themes/vesper.json +0 -218
  149. package/src/theme/themes/zenburn.json +0 -223
  150. package/src/theme/types.ts +0 -119
  151. package/src/types/message.ts +0 -97
  152. package/src/ui/ChafaImage.tsx +0 -64
  153. package/src/ui/Splash.tsx +0 -118
  154. package/src/ui/borders.ts +0 -28
  155. package/src/ui/command.tsx +0 -104
  156. package/src/ui/dialog-select.tsx +0 -164
  157. package/src/ui/dialog.tsx +0 -102
  158. package/src/ui/fmt.ts +0 -82
  159. package/src/ui/kv.tsx +0 -28
  160. package/src/ui/shell.tsx +0 -45
  161. package/src/ui/spinner.tsx +0 -59
  162. package/src/ui/splash-art.ts +0 -123
  163. package/src/ui/table.tsx +0 -117
  164. package/src/ui/ticker.tsx +0 -90
  165. package/src/ui/toast.tsx +0 -130
  166. package/src/utils/categorical.ts +0 -77
  167. package/src/utils/chafa.ts +0 -173
  168. package/src/utils/clipboard.ts +0 -67
  169. package/src/utils/context-segments.ts +0 -317
  170. package/src/utils/control.ts +0 -495
  171. package/src/utils/drop.ts +0 -25
  172. package/src/utils/editor.ts +0 -33
  173. package/src/utils/fuzzy.ts +0 -45
  174. package/src/utils/gateway-client.ts +0 -253
  175. package/src/utils/gateway-types.ts +0 -282
  176. package/src/utils/git.ts +0 -57
  177. package/src/utils/hermes-analytics.ts +0 -134
  178. package/src/utils/hermes-home.ts +0 -821
  179. package/src/utils/hermes-kanban.ts +0 -154
  180. package/src/utils/hermes-profiles.ts +0 -217
  181. package/src/utils/interpolate.ts +0 -31
  182. package/src/utils/math-unicode.ts +0 -818
  183. package/src/utils/memory-activity.ts +0 -140
  184. package/src/utils/open-file.ts +0 -13
  185. package/src/utils/paths.ts +0 -52
  186. package/src/utils/perf.ts +0 -235
  187. package/src/utils/preferences.ts +0 -150
  188. package/src/utils/sessions-db.ts +0 -396
  189. package/src/utils/subagent-tree.ts +0 -146
  190. package/src/utils/terminal-reset.ts +0 -129
  191. package/src/utils/tips.ts +0 -67
  192. package/src/utils/tokens.ts +0 -87
@@ -1,140 +0,0 @@
1
- // Recent memory-tool invocations scraped from state.db.
2
- //
3
- // No gateway RPC or audit log exists for this; messages.tool_calls on
4
- // assistant rows carries the full invocation JSON, so read sqlite
5
- // directly (same pattern as hermes-analytics.ts).
6
-
7
- import { Database } from "bun:sqlite"
8
- import { hermesPath } from "./hermes-home"
9
-
10
- type MemoryOp = "write" | "read"
11
-
12
- export type MemoryActivity = {
13
- ts: number
14
- provider: string
15
- tool: string
16
- op: MemoryOp
17
- /** Human verb: add, replace, remove, conclude, search, … */
18
- verb: string
19
- /** Short payload summary (query text, content head, target). */
20
- summary: string
21
- sessionId: string
22
- sessionTitle: string
23
- }
24
-
25
- // Tool-name → provider. Built from plugins/memory/*/__init__.py tool
26
- // defs + the core `memory` tool.
27
- const WRITE: Record<string, string> = {
28
- memory: "builtin",
29
- mem0_conclude: "mem0",
30
- honcho_conclude: "honcho",
31
- hindsight_retain: "hindsight", hindsight_reflect: "hindsight",
32
- fact_store: "holographic", fact_feedback: "holographic",
33
- viking_remember: "openviking", viking_add_resource: "openviking",
34
- retaindb_remember: "retaindb", retaindb_forget: "retaindb",
35
- supermemory_store: "supermemory", supermemory_forget: "supermemory",
36
- brv_curate: "byterover",
37
- }
38
- const READ: Record<string, string> = {
39
- mem0_search: "mem0", mem0_profile: "mem0",
40
- honcho_search: "honcho", honcho_profile: "honcho",
41
- honcho_reasoning: "honcho", honcho_context: "honcho",
42
- hindsight_recall: "hindsight",
43
- viking_search: "openviking", viking_read: "openviking", viking_browse: "openviking",
44
- retaindb_search: "retaindb", retaindb_profile: "retaindb", retaindb_context: "retaindb",
45
- supermemory_search: "supermemory", supermemory_profile: "supermemory",
46
- brv_query: "byterover", brv_status: "byterover",
47
- }
48
-
49
- const MEMORY_TOOLS = { ...WRITE, ...READ }
50
-
51
- const trunc = (s: unknown, n = 80): string => {
52
- const t = String(s ?? "").replace(/\s+/g, " ").trim()
53
- return t.length > n ? t.slice(0, n - 1) + "…" : t
54
- }
55
-
56
- const stripPrefix = (name: string): string =>
57
- name.replace(/^(mem0|honcho|hindsight|viking|retaindb|supermemory|brv|fact)_/, "")
58
-
59
- type Args = Record<string, unknown>
60
-
61
- const describe = (name: string, args: Args): { verb: string; summary: string } => {
62
- if (name === "memory") {
63
- const action = String(args.action ?? "")
64
- const target = String(args.target ?? "")
65
- const body = action === "remove" ? args.old_text : args.content ?? args.old_text
66
- return { verb: action, summary: `${target}: ${trunc(body)}` }
67
- }
68
- const verb = stripPrefix(name)
69
- for (const k of ["conclusion", "content", "query", "text", "fact", "question", "note", "path"])
70
- if (k in args) return { verb, summary: trunc(args[k]) }
71
- const first = Object.values(args).find(v => typeof v === "string")
72
- return { verb, summary: trunc(first ?? "") }
73
- }
74
-
75
- type Row = {
76
- ts: number
77
- tool_calls: string
78
- session_id: string
79
- title: string | null
80
- }
81
-
82
- /** Parse memory-tool calls out of a single assistant row. Exported for test. */
83
- export const extract = (r: Row): MemoryActivity[] => {
84
- let calls: Array<{ function?: { name?: string; arguments?: string } }>
85
- try { calls = JSON.parse(r.tool_calls) } catch { return [] }
86
- if (!Array.isArray(calls)) return []
87
- const out: MemoryActivity[] = []
88
- for (const c of calls) {
89
- const name = c.function?.name
90
- if (!name || !(name in MEMORY_TOOLS)) continue
91
- let args: Args = {}
92
- try { args = JSON.parse(c.function?.arguments ?? "{}") } catch { /* keep {} */ }
93
- const { verb, summary } = describe(name, args)
94
- out.push({
95
- ts: r.ts,
96
- provider: MEMORY_TOOLS[name],
97
- tool: name,
98
- op: name in WRITE ? "write" : "read",
99
- verb, summary,
100
- sessionId: r.session_id,
101
- sessionTitle: r.title ?? r.session_id,
102
- })
103
- }
104
- return out
105
- }
106
-
107
- /**
108
- * Scan recent assistant rows for memory-tool invocations.
109
- *
110
- * Bounded by `scan` (row window), not DB size — tool_calls isn't indexed
111
- * and json_each over the whole table is unbounded. 2000 rows ≈ a few
112
- * days of heavy use.
113
- */
114
- export function readMemoryActivity(limit = 100, scan = 2000): MemoryActivity[] {
115
- let db: Database
116
- try {
117
- db = new Database(hermesPath("state.db"), { readonly: true })
118
- } catch {
119
- return []
120
- }
121
- try {
122
- const rows = db.query<Row, [number]>(
123
- `SELECT m.timestamp ts, m.tool_calls, m.session_id,
124
- s.title
125
- FROM messages m LEFT JOIN sessions s ON m.session_id = s.id
126
- WHERE m.role = 'assistant' AND m.tool_calls IS NOT NULL
127
- ORDER BY m.id DESC LIMIT ?`,
128
- ).all(scan)
129
- const out: MemoryActivity[] = []
130
- for (const r of rows) {
131
- for (const a of extract(r)) {
132
- out.push(a)
133
- if (out.length >= limit) return out
134
- }
135
- }
136
- return out
137
- } finally {
138
- db.close()
139
- }
140
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * open-file.ts — Open files and URLs using the OS default handler.
3
- *
4
- * Uses the `open` package (cross-platform: xdg-open on Linux, open on macOS, start on Windows).
5
- * Fire-and-forget — does not block the TUI.
6
- */
7
-
8
- import open from "open";
9
-
10
- /** Open a file in the OS default handler for its type */
11
- export function openFile(path: string): void {
12
- open(path).catch(() => {});
13
- }
@@ -1,52 +0,0 @@
1
- // Filesystem locations herm writes to. Central so the default stays
2
- // consistent across callers and the legacy-path migration only lives
3
- // in one spot.
4
- //
5
- // Defaults
6
- // HERM_CONFIG_DIR → $HERMES_HOME/herm (typically ~/.hermes/herm)
7
- // HERMES_HOME → ~/.hermes
8
- //
9
- // Legacy layout (pre-0.1): ~/.config/herm — we auto-migrate any files
10
- // sitting there into the new location on first access, once.
11
-
12
- import { homedir } from "os"
13
- import { join } from "path"
14
- import { existsSync, mkdirSync, readdirSync, renameSync } from "fs"
15
-
16
- const HOME = () => process.env.HOME || homedir()
17
- const HERMES_HOME = () => process.env.HERMES_HOME || join(HOME(), ".hermes")
18
-
19
- let migrated = false
20
-
21
- /** Where herm-specific prefs, history, and caches live. */
22
- export function configDir(): string {
23
- const dir = process.env.HERM_CONFIG_DIR || join(HERMES_HOME(), "herm")
24
- if (!migrated) {
25
- migrated = true
26
- maybeMigrateLegacy(dir)
27
- }
28
- return dir
29
- }
30
-
31
- /** One-time migration: ~/.config/herm/* → new configDir if empty. */
32
- function maybeMigrateLegacy(target: string): void {
33
- // Respect explicit override: if the user set HERM_CONFIG_DIR we
34
- // never touch the legacy path.
35
- if (process.env.HERM_CONFIG_DIR) return
36
- const legacy = join(HOME(), ".config", "herm")
37
- if (!existsSync(legacy) || legacy === target) return
38
- // Only migrate when the target doesn't already hold data — never
39
- // clobber a fresh install.
40
- try {
41
- if (existsSync(target) && readdirSync(target).length > 0) return
42
- mkdirSync(target, { recursive: true })
43
- for (const name of readdirSync(legacy)) {
44
- const src = join(legacy, name)
45
- const dst = join(target, name)
46
- if (existsSync(dst)) continue
47
- try { renameSync(src, dst) } catch { /* cross-device or locked — skip */ }
48
- }
49
- } catch {
50
- // Best-effort; a failed migration should never block startup.
51
- }
52
- }
package/src/utils/perf.ts DELETED
@@ -1,235 +0,0 @@
1
- /**
2
- * perf.ts — Zero-cost profiling for herm.
3
- *
4
- * Gate: set PERF=1 to activate. When disabled, every export
5
- * is a no-op or passthrough — no allocations, no timers, no overhead.
6
- *
7
- * Usage:
8
- * PERF=1 bun run dev # periodic memory + event stats
9
- * PERF=verbose bun run dev # ^ plus per-event timing logs
10
- *
11
- * API:
12
- * mark(label) — start timing, returns end() → ms
13
- * count(label) — increment a named counter
14
- * mem(label) — snapshot RSS/heap at a labeled point
15
- * monitor(ms) — start periodic memory reporter, returns cleanup
16
- * report() — dump all collected stats to stderr
17
- * onRender(...) — React <Profiler> onRender callback
18
- */
19
-
20
- const level = process.env.PERF ?? ""
21
- const enabled = level === "1" || level === "verbose"
22
- const verbose = level === "verbose"
23
-
24
- // ── Timing ────────────────────────────────────────────────────────────
25
-
26
- type Timing = { count: number; total: number; min: number; max: number; last: number }
27
-
28
- const timings = new Map<string, Timing>()
29
-
30
- const noop = () => 0
31
-
32
- /** Start a timing mark. Returns end() which returns elapsed ms. */
33
- export const mark = enabled
34
- ? (label: string): (() => number) => {
35
- const start = Bun.nanoseconds()
36
- return () => {
37
- const ms = (Bun.nanoseconds() - start) / 1e6
38
- const t = timings.get(label)
39
- if (t) {
40
- t.count++
41
- t.total += ms
42
- if (ms < t.min) t.min = ms
43
- if (ms > t.max) t.max = ms
44
- t.last = ms
45
- } else {
46
- timings.set(label, { count: 1, total: ms, min: ms, max: ms, last: ms })
47
- }
48
- if (verbose) log(`⏱ ${label}: ${ms.toFixed(2)}ms`)
49
- return ms
50
- }
51
- }
52
- : (_: string) => noop
53
-
54
- // ── Boot stages ───────────────────────────────────────────────────────
55
- // One-shot milestones measured from process start (Bun.nanoseconds()
56
- // origin). ESM imports hoist, so mark() can't bracket the import graph;
57
- // callers pass the absolute ms-since-spawn instead.
58
- const stages: Array<[string, number]> = []
59
- export const boot = (label: string, ms: number) => {
60
- stages.push([label, ms])
61
- if (enabled) log(`🚀 boot:${label} ${ms.toFixed(1)}ms`)
62
- }
63
-
64
- // ── Counters ──────────────────────────────────────────────────────────
65
-
66
- const counters = new Map<string, number>()
67
-
68
- /** Increment a named counter. */
69
- export const count = enabled
70
- ? (label: string, n = 1) => {
71
- counters.set(label, (counters.get(label) ?? 0) + n)
72
- }
73
- : (_label: string, _n?: number) => {}
74
-
75
- // ── Memory ────────────────────────────────────────────────────────────
76
-
77
- type Snapshot = { label: string; rss: number; heap: number; external: number; ts: number }
78
-
79
- const snapshots: Snapshot[] = []
80
-
81
- const mb = (n: number) => (n / 1024 / 1024).toFixed(1)
82
-
83
- /** Snapshot memory at a labeled point. */
84
- export const mem = enabled
85
- ? (label: string) => {
86
- const m = process.memoryUsage()
87
- snapshots.push({ label, rss: m.rss, heap: m.heapUsed, external: m.external, ts: Date.now() })
88
- log(`📊 [${label}] RSS=${mb(m.rss)}MB heap=${mb(m.heapUsed)}MB ext=${mb(m.external)}MB`)
89
- }
90
- : (_: string) => {}
91
-
92
- /** Start periodic memory reporter. Returns cleanup function. */
93
- export const monitor = enabled
94
- ? (ms = 10_000): (() => void) => {
95
- const id = setInterval(() => {
96
- const m = process.memoryUsage()
97
- const gc = Bun.gc(false)
98
- log(
99
- `\x1b[90m[mem] RSS=${mb(m.rss)}MB heap=${mb(m.heapUsed)}/${mb(m.heapTotal)}MB`
100
- + ` ext=${mb(m.external)}MB gcRuns=${(gc as unknown as Record<string, unknown>)?.eden_collections ?? "?"}/${(gc as unknown as Record<string, unknown>)?.full_collections ?? "?"}\x1b[0m`
101
- )
102
- }, ms)
103
- return () => clearInterval(id)
104
- }
105
- : (_ms?: number) => noop
106
-
107
- // ── React Profiler ────────────────────────────────────────────────────
108
-
109
- type RenderEntry = { count: number; total: number; max: number; last: number }
110
-
111
- const renders = new Map<string, RenderEntry>()
112
-
113
- /**
114
- * Drop-in for React <Profiler onRender={perf.onRender}>.
115
- * Tracks render count, total time, and max render time per id.
116
- */
117
- export const onRender = enabled
118
- ? (id: string, phase: "mount" | "update" | "nested-update", actual: number) => {
119
- const r = renders.get(id)
120
- if (r) {
121
- r.count++
122
- r.total += actual
123
- if (actual > r.max) r.max = actual
124
- r.last = actual
125
- } else {
126
- renders.set(id, { count: 1, total: actual, max: actual, last: actual })
127
- }
128
- if (verbose && actual > 1) {
129
- log(`🔄 [${id}] ${phase}: ${actual.toFixed(2)}ms`)
130
- }
131
- }
132
- : (_id: string, _phase: string, _actual: number) => {}
133
-
134
- // ── Report ────────────────────────────────────────────────────────────
135
-
136
- /** Dump all collected profiling data to stderr. */
137
- const report = () => {
138
- if (!enabled) return
139
-
140
- const lines: string[] = ["\n\x1b[1m═══ PERF REPORT ═══\x1b[0m\n"]
141
-
142
- // Memory snapshots
143
- if (snapshots.length > 0) {
144
- lines.push("\x1b[1mMemory Snapshots:\x1b[0m")
145
- for (const s of snapshots) {
146
- lines.push(` ${s.label}: RSS=${mb(s.rss)}MB heap=${mb(s.heap)}MB ext=${mb(s.external)}MB`)
147
- }
148
- const first = snapshots[0]
149
- const last = snapshots[snapshots.length - 1]
150
- const drift = last.rss - first.rss
151
- lines.push(` Δ RSS: ${drift > 0 ? "+" : ""}${mb(drift)}MB (${first.label} → ${last.label})`)
152
- lines.push("")
153
- }
154
-
155
- // Timings
156
- if (timings.size > 0) {
157
- lines.push("\x1b[1mTimings:\x1b[0m")
158
- const sorted = [...timings.entries()].sort((a, b) => b[1].total - a[1].total)
159
- for (const [label, t] of sorted) {
160
- const avg = t.total / t.count
161
- lines.push(
162
- ` ${label}: ${t.count}× avg=${avg.toFixed(2)}ms`
163
- + ` min=${t.min.toFixed(2)}ms max=${t.max.toFixed(2)}ms total=${t.total.toFixed(0)}ms`
164
- )
165
- }
166
- lines.push("")
167
- }
168
-
169
- // Render profiler
170
- if (renders.size > 0) {
171
- lines.push("\x1b[1mReact Renders:\x1b[0m")
172
- const sorted = [...renders.entries()].sort((a, b) => b[1].count - a[1].count)
173
- for (const [id, r] of sorted) {
174
- const avg = r.total / r.count
175
- lines.push(
176
- ` <${id}>: ${r.count}× avg=${avg.toFixed(2)}ms max=${r.max.toFixed(2)}ms total=${r.total.toFixed(0)}ms`
177
- )
178
- }
179
- lines.push("")
180
- }
181
-
182
- // Counters
183
- if (counters.size > 0) {
184
- lines.push("\x1b[1mCounters:\x1b[0m")
185
- const sorted = [...counters.entries()].sort((a, b) => b[1] - a[1])
186
- for (const [label, n] of sorted) {
187
- lines.push(` ${label}: ${n}`)
188
- }
189
- lines.push("")
190
- }
191
-
192
- log(lines.join("\n"))
193
- }
194
-
195
- /** Return all profiling data as a plain object (for JSON API). */
196
- export const data = () => {
197
- if (!enabled) return null
198
- const m = process.memoryUsage()
199
- return {
200
- boot: Object.fromEntries(stages.map(([l, ms]) => [l, +ms.toFixed(1)])),
201
- memory: {
202
- rss: Math.round(m.rss / 1024 / 1024),
203
- heap: Math.round(m.heapUsed / 1024 / 1024),
204
- heapTotal: Math.round(m.heapTotal / 1024 / 1024),
205
- external: Math.round(m.external / 1024 / 1024),
206
- },
207
- snapshots: snapshots.map(s => ({
208
- label: s.label,
209
- rss: Math.round(s.rss / 1024 / 1024),
210
- heap: Math.round(s.heap / 1024 / 1024),
211
- external: Math.round(s.external / 1024 / 1024),
212
- })),
213
- timings: Object.fromEntries(
214
- [...timings.entries()].sort((a, b) => b[1].total - a[1].total)
215
- .map(([k, v]) => [k, { count: v.count, avg: +(v.total / v.count).toFixed(2), min: +v.min.toFixed(2), max: +v.max.toFixed(2), total: Math.round(v.total) }])
216
- ),
217
- renders: Object.fromEntries(
218
- [...renders.entries()].sort((a, b) => b[1].count - a[1].count)
219
- .map(([k, v]) => [k, { count: v.count, avg: +(v.total / v.count).toFixed(2), max: +v.max.toFixed(2), total: Math.round(v.total) }])
220
- ),
221
- counters: Object.fromEntries(
222
- [...counters.entries()].sort((a, b) => b[1] - a[1])
223
- ),
224
- }
225
- }
226
-
227
- // ── Internal ──────────────────────────────────────────────────────────
228
-
229
- const log = (msg: string) => process.stderr.write(msg + "\n")
230
-
231
- // Dump report on exit
232
- if (enabled) {
233
- process.on("exit", report)
234
- process.on("SIGINT", () => { report(); process.exit(0) })
235
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * Local TUI preferences — persisted to ~/.config/herm/tui.json
3
- *
4
- * Compatible with OpenCode's tui.json schema pattern:
5
- * - JSON file in XDG config dir
6
- * - Optional fields with sensible defaults
7
- * - Deep-merged from multiple sources (global → project)
8
- * - Read once at startup, written on change
9
- *
10
- * Herm-specific extensions (beyond OpenCode compat):
11
- * - lastSessionId: resume previous session on startup
12
- */
13
-
14
- import { join } from "path"
15
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
16
- import { useSyncExternalStore } from "react"
17
-
18
- // ─── Schema ──────────────────────────────────────────────────────────
19
-
20
- export type DetailMode = "hidden" | "collapsed" | "expanded"
21
-
22
- interface TuiPreferences {
23
- /** JSON schema reference (for editor autocomplete) */
24
- $schema?: string
25
- /** Theme name — must match a built-in or custom theme */
26
- theme?: string
27
- /** Mouse capture enabled */
28
- mouse?: boolean
29
- /** Target render FPS */
30
- targetFps?: number
31
-
32
- // ─── Herm extensions ─────────────────────────────────────────────
33
- /** Last active session ID — stub-reuse check on fresh launch */
34
- lastSessionId?: string
35
- /** Path to a .eikon avatar file for the sidebar */
36
- eikonPath?: string
37
- /** Spinner/avatar frame animations (off → static glyphs) */
38
- animations?: boolean
39
- /** Thought-cloud tool trail verbosity */
40
- toolDetails?: DetailMode
41
- /** User keybinding overrides (ActionId → chord string) */
42
- keys?: Record<string, string>
43
- /** Clock style for time-of-day formatters */
44
- timeFormat?: "12h" | "24h"
45
- /** List-column timestamps: "2h ago" vs "14:32" / "May 1" */
46
- timeStyle?: "relative" | "absolute"
47
- }
48
-
49
- const DEFAULTS: Required<Pick<TuiPreferences, "mouse" | "targetFps">> = {
50
- mouse: true,
51
- targetFps: 30,
52
- }
53
-
54
- // ─── Paths ───────────────────────────────────────────────────────────
55
-
56
- import { configDir } from "./paths"
57
-
58
- function configFile() { return join(configDir(), "tui.json") }
59
-
60
- // ─── Load ────────────────────────────────────────────────────────────
61
-
62
- let cached: TuiPreferences | null = null
63
-
64
- /** Test-only: drop the cached snapshot so the next load() re-reads disk. */
65
- export function reset(): void {
66
- cached = null
67
- }
68
-
69
- /**
70
- * Load preferences from disk. Returns cached copy on subsequent calls.
71
- * Never throws — returns defaults on missing/corrupt file.
72
- */
73
- export function load(): TuiPreferences {
74
- if (cached) return cached
75
-
76
- const CONFIG_FILE = configFile()
77
- try {
78
- if (!existsSync(CONFIG_FILE)) {
79
- const prefs = { ...DEFAULTS }
80
- cached = prefs
81
- return prefs
82
- }
83
- const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
84
- const prefs = { ...DEFAULTS, ...raw }
85
- cached = prefs
86
- return prefs
87
- } catch {
88
- const prefs = { ...DEFAULTS }
89
- cached = prefs
90
- return prefs
91
- }
92
- }
93
-
94
- // ─── Save ────────────────────────────────────────────────────────────
95
-
96
- /**
97
- * Persist current preferences to disk.
98
- * Merges provided partial into existing prefs before writing.
99
- */
100
- function save(partial?: Partial<TuiPreferences>): void {
101
- const current = load()
102
- if (partial) Object.assign(current, partial)
103
- cached = current
104
-
105
- try {
106
- const CONFIG_DIR = configDir()
107
- if (!existsSync(CONFIG_DIR)) {
108
- mkdirSync(CONFIG_DIR, { recursive: true })
109
- }
110
- // Write with sorted keys for stable diffs
111
- const json = JSON.stringify(current, null, 2) + "\n"
112
- writeFileSync(configFile(), json, "utf-8")
113
- } catch (err) {
114
- // Silently fail — preferences are non-critical
115
- if (process.env.PERF) {
116
- console.error("[preferences] failed to save:", err)
117
- }
118
- }
119
- }
120
-
121
- // ─── Convenience ─────────────────────────────────────────────────────
122
-
123
- /** Get a single preference value */
124
- export function get<K extends keyof TuiPreferences>(key: K): TuiPreferences[K] {
125
- return load()[key]
126
- }
127
-
128
- /** Set a single preference value and persist */
129
- export function set<K extends keyof TuiPreferences>(key: K, value: TuiPreferences[K]): void {
130
- save({ [key]: value } as Partial<TuiPreferences>)
131
- for (const l of listeners) l()
132
- }
133
-
134
- // ─── Reactive ────────────────────────────────────────────────────────
135
-
136
- const listeners = new Set<() => void>()
137
-
138
- function subscribe(l: () => void): () => void {
139
- listeners.add(l)
140
- return () => listeners.delete(l)
141
- }
142
-
143
- /**
144
- * Subscribe a component to a preference key. Re-renders on set().
145
- * Writes go through the imperative `set(key, value)` — this hook is
146
- * read-only by design so writes always persist through one path.
147
- */
148
- export function usePref<K extends keyof TuiPreferences>(key: K): TuiPreferences[K] {
149
- return useSyncExternalStore(subscribe, () => load()[key])
150
- }