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.
- package/db.worker.js +81 -0
- package/highlights-eq9cgrbb.scm +604 -0
- package/highlights-ghv9g403.scm +205 -0
- package/highlights-hk7bwhj4.scm +284 -0
- package/highlights-r812a2qc.scm +150 -0
- package/highlights-x6tmsnaa.scm +115 -0
- package/index.js +10374 -0
- package/injections-73j83es3.scm +27 -0
- package/package.json +14 -64
- package/parser.worker.js +8 -0
- package/tree-sitter-3jzf13jk.wasm +0 -0
- package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/scripts/postinstall.ts +0 -29
- package/src/app/gateway.tsx +0 -83
- package/src/app/gatewayEvents.ts +0 -203
- package/src/app/launch.ts +0 -41
- package/src/app/skin.tsx +0 -31
- package/src/app/spawnHistory.ts +0 -75
- package/src/app/tabs.ts +0 -23
- package/src/app/turnReducer.ts +0 -390
- package/src/app/useAppKeys.ts +0 -268
- package/src/app/useAtRefPopover.ts +0 -99
- package/src/app/useInputHistory.ts +0 -66
- package/src/app/useSession.ts +0 -102
- package/src/app/useSlashCommands.ts +0 -70
- package/src/app/useSlashPopover.ts +0 -48
- package/src/app.tsx +0 -917
- package/src/commands/slash.ts +0 -151
- package/src/components/avatar/AnimatedAvatar.tsx +0 -66
- package/src/components/avatar/eikon.ts +0 -144
- package/src/components/avatar/states/error.ts +0 -1155
- package/src/components/avatar/states/idle.ts +0 -1155
- package/src/components/avatar/states/index.ts +0 -30
- package/src/components/avatar/states/listening.ts +0 -1155
- package/src/components/avatar/states/speaking.ts +0 -1155
- package/src/components/avatar/states/thinking.ts +0 -1155
- package/src/components/avatar/states/working.ts +0 -1155
- package/src/components/chat/AtRefPopover.tsx +0 -54
- package/src/components/chat/CodeBlock.tsx +0 -67
- package/src/components/chat/Composer.tsx +0 -347
- package/src/components/chat/DiffBlock.tsx +0 -116
- package/src/components/chat/ErrorBlock.tsx +0 -70
- package/src/components/chat/MediaChip.tsx +0 -114
- package/src/components/chat/MessageItem.tsx +0 -282
- package/src/components/chat/MessageList.tsx +0 -114
- package/src/components/chat/PromptCard.tsx +0 -359
- package/src/components/chat/SlashPopover.tsx +0 -158
- package/src/components/chat/ThoughtCloud.tsx +0 -185
- package/src/components/chat/TypingIndicator.tsx +0 -25
- package/src/components/chat/tool/Subagent.tsx +0 -75
- package/src/components/chat/tool/frame.tsx +0 -69
- package/src/components/chat/tool/index.tsx +0 -65
- package/src/components/chat/tool/preview.ts +0 -57
- package/src/components/sidebar/ContextGauge.tsx +0 -102
- package/src/components/sidebar/Sidebar.tsx +0 -143
- package/src/components/tabs/TabBar.tsx +0 -50
- package/src/components/ui/FileLink.tsx +0 -52
- package/src/config/index.ts +0 -156
- package/src/config/lane.ts +0 -161
- package/src/config/models.ts +0 -95
- package/src/config/rules.ts +0 -80
- package/src/config/schema.ts +0 -308
- package/src/dialogs/alert.tsx +0 -52
- package/src/dialogs/chafa.tsx +0 -72
- package/src/dialogs/confirm.tsx +0 -58
- package/src/dialogs/curator.tsx +0 -153
- package/src/dialogs/eikon-picker.tsx +0 -95
- package/src/dialogs/help.tsx +0 -80
- package/src/dialogs/history.tsx +0 -92
- package/src/dialogs/info.tsx +0 -115
- package/src/dialogs/keys.tsx +0 -170
- package/src/dialogs/logs.tsx +0 -42
- package/src/dialogs/message.tsx +0 -38
- package/src/dialogs/model-picker.tsx +0 -123
- package/src/dialogs/new-profile.tsx +0 -69
- package/src/dialogs/new-task.tsx +0 -103
- package/src/dialogs/profile.tsx +0 -55
- package/src/dialogs/rollback.tsx +0 -190
- package/src/dialogs/spawn-history.tsx +0 -80
- package/src/dialogs/text-prompt.tsx +0 -68
- package/src/dialogs/theme-picker.tsx +0 -50
- package/src/home/index.ts +0 -23
- package/src/home/store.ts +0 -267
- package/src/index.tsx +0 -113
- package/src/keys/catalog.ts +0 -115
- package/src/keys/chord.ts +0 -125
- package/src/keys/conflicts.ts +0 -48
- package/src/keys/context.tsx +0 -112
- package/src/keys/index.ts +0 -5
- package/src/keys/list.ts +0 -94
- package/src/keys/oc-compat.ts +0 -87
- package/src/tabs/Agents.tsx +0 -607
- package/src/tabs/Analytics.tsx +0 -154
- package/src/tabs/Chat.tsx +0 -50
- package/src/tabs/Config.tsx +0 -605
- package/src/tabs/Context.tsx +0 -599
- package/src/tabs/Cron.tsx +0 -294
- package/src/tabs/Env.tsx +0 -227
- package/src/tabs/Kanban.tsx +0 -367
- package/src/tabs/Memory.tsx +0 -294
- package/src/tabs/Sessions.tsx +0 -786
- package/src/tabs/Skills.tsx +0 -507
- package/src/tabs/Toolsets.tsx +0 -266
- package/src/theme/builtin.ts +0 -78
- package/src/theme/context.tsx +0 -106
- package/src/theme/index.ts +0 -4
- package/src/theme/resolve.ts +0 -134
- package/src/theme/syntax.ts +0 -31
- package/src/theme/themes/aura.json +0 -69
- package/src/theme/themes/ayu.json +0 -80
- package/src/theme/themes/carbonfox.json +0 -248
- package/src/theme/themes/catppuccin-frappe.json +0 -233
- package/src/theme/themes/catppuccin-macchiato.json +0 -233
- package/src/theme/themes/catppuccin.json +0 -112
- package/src/theme/themes/cobalt2.json +0 -228
- package/src/theme/themes/cursor.json +0 -249
- package/src/theme/themes/dracula.json +0 -219
- package/src/theme/themes/everforest.json +0 -241
- package/src/theme/themes/flexoki.json +0 -237
- package/src/theme/themes/github.json +0 -233
- package/src/theme/themes/gruvbox.json +0 -242
- package/src/theme/themes/kanagawa.json +0 -77
- package/src/theme/themes/lucent-orng.json +0 -237
- package/src/theme/themes/material.json +0 -235
- package/src/theme/themes/matrix.json +0 -77
- package/src/theme/themes/mercury.json +0 -252
- package/src/theme/themes/monokai.json +0 -221
- package/src/theme/themes/nightowl.json +0 -221
- package/src/theme/themes/nord.json +0 -223
- package/src/theme/themes/one-dark.json +0 -84
- package/src/theme/themes/opencode.json +0 -245
- package/src/theme/themes/orng.json +0 -249
- package/src/theme/themes/osaka-jade.json +0 -93
- package/src/theme/themes/palenight.json +0 -222
- package/src/theme/themes/rosepine.json +0 -234
- package/src/theme/themes/solarized.json +0 -223
- package/src/theme/themes/synthwave84.json +0 -226
- package/src/theme/themes/tokyonight.json +0 -243
- package/src/theme/themes/vercel.json +0 -245
- package/src/theme/themes/vesper.json +0 -218
- package/src/theme/themes/zenburn.json +0 -223
- package/src/theme/types.ts +0 -119
- package/src/types/message.ts +0 -97
- package/src/ui/ChafaImage.tsx +0 -64
- package/src/ui/Splash.tsx +0 -118
- package/src/ui/borders.ts +0 -28
- package/src/ui/command.tsx +0 -104
- package/src/ui/dialog-select.tsx +0 -164
- package/src/ui/dialog.tsx +0 -102
- package/src/ui/fmt.ts +0 -82
- package/src/ui/kv.tsx +0 -28
- package/src/ui/shell.tsx +0 -45
- package/src/ui/spinner.tsx +0 -59
- package/src/ui/splash-art.ts +0 -123
- package/src/ui/table.tsx +0 -117
- package/src/ui/ticker.tsx +0 -90
- package/src/ui/toast.tsx +0 -130
- package/src/utils/categorical.ts +0 -77
- package/src/utils/chafa.ts +0 -173
- package/src/utils/clipboard.ts +0 -67
- package/src/utils/context-segments.ts +0 -317
- package/src/utils/control.ts +0 -495
- package/src/utils/drop.ts +0 -25
- package/src/utils/editor.ts +0 -33
- package/src/utils/fuzzy.ts +0 -45
- package/src/utils/gateway-client.ts +0 -253
- package/src/utils/gateway-types.ts +0 -282
- package/src/utils/git.ts +0 -57
- package/src/utils/hermes-analytics.ts +0 -134
- package/src/utils/hermes-home.ts +0 -821
- package/src/utils/hermes-kanban.ts +0 -154
- package/src/utils/hermes-profiles.ts +0 -217
- package/src/utils/interpolate.ts +0 -31
- package/src/utils/math-unicode.ts +0 -818
- package/src/utils/memory-activity.ts +0 -140
- package/src/utils/open-file.ts +0 -13
- package/src/utils/paths.ts +0 -52
- package/src/utils/perf.ts +0 -235
- package/src/utils/preferences.ts +0 -150
- package/src/utils/sessions-db.ts +0 -396
- package/src/utils/subagent-tree.ts +0 -146
- package/src/utils/terminal-reset.ts +0 -129
- package/src/utils/tips.ts +0 -67
- 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
|
-
}
|
package/src/utils/open-file.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/paths.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/preferences.ts
DELETED
|
@@ -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
|
-
}
|