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