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,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
- }