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
package/src/utils/chafa.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Parse a single line of chafa `--format=symbols --colors=full` output
|
|
2
|
-
// into renderable cells. chafa emits SGR escapes interleaved with cell
|
|
3
|
-
// characters. Each cell is a single Unicode codepoint preceded by zero or
|
|
4
|
-
// more SGR sequences that set fg/bg/reverse. We flatten that stream into
|
|
5
|
-
// { ch, fg, bg } records and let the caller build spans.
|
|
6
|
-
//
|
|
7
|
-
// Grammar (informal):
|
|
8
|
-
// LINE := (SGR* CELL)*
|
|
9
|
-
// SGR := ESC '[' PARAMS 'm'
|
|
10
|
-
// PARAMS := N (';' N)* // e.g. "38;2;255;0;0" or "0" or "7"
|
|
11
|
-
// CELL := any single codepoint, not ESC
|
|
12
|
-
//
|
|
13
|
-
// State mutations from SGR params, reading left to right:
|
|
14
|
-
// 0 → fg = bg = null; reverse = false
|
|
15
|
-
// 7 → reverse = true (swap fg/bg for following cells)
|
|
16
|
-
// 38;2;R;G;B → fg = rgb(R,G,B)
|
|
17
|
-
// 48;2;R;G;B → bg = rgb(R,G,B)
|
|
18
|
-
// everything else is ignored (chafa doesn't emit 256-color in full mode,
|
|
19
|
-
// and basic 16-color shouldn't appear — but silent-skip is safer than
|
|
20
|
-
// throw on an unexpected byte).
|
|
21
|
-
|
|
22
|
-
export type RGB = { r: number; g: number; b: number }
|
|
23
|
-
export type Cell = { ch: string; fg: RGB | null; bg: RGB | null }
|
|
24
|
-
|
|
25
|
-
const ESC = 0x1b
|
|
26
|
-
const LSQ = 0x5b // '['
|
|
27
|
-
|
|
28
|
-
export function parseChafaLine(line: string): Cell[] {
|
|
29
|
-
const out: Cell[] = []
|
|
30
|
-
let fg: RGB | null = null
|
|
31
|
-
let bg: RGB | null = null
|
|
32
|
-
let reverse = false
|
|
33
|
-
let i = 0
|
|
34
|
-
const N = line.length
|
|
35
|
-
|
|
36
|
-
while (i < N) {
|
|
37
|
-
const code = line.charCodeAt(i)
|
|
38
|
-
// SGR run
|
|
39
|
-
if (code === ESC && line.charCodeAt(i + 1) === LSQ) {
|
|
40
|
-
const end = line.indexOf("m", i + 2)
|
|
41
|
-
if (end < 0) { i = N; break }
|
|
42
|
-
const params = line.slice(i + 2, end).split(";").map(x => parseInt(x, 10) || 0)
|
|
43
|
-
let p = 0
|
|
44
|
-
while (p < params.length) {
|
|
45
|
-
const n = params[p]
|
|
46
|
-
if (n === 0) { fg = null; bg = null; reverse = false; p++; continue }
|
|
47
|
-
if (n === 7) { reverse = true; p++; continue }
|
|
48
|
-
if (n === 27) { reverse = false; p++; continue }
|
|
49
|
-
if (n === 38 && params[p + 1] === 2) {
|
|
50
|
-
fg = { r: params[p + 2] | 0, g: params[p + 3] | 0, b: params[p + 4] | 0 }
|
|
51
|
-
p += 5; continue
|
|
52
|
-
}
|
|
53
|
-
if (n === 48 && params[p + 1] === 2) {
|
|
54
|
-
bg = { r: params[p + 2] | 0, g: params[p + 3] | 0, b: params[p + 4] | 0 }
|
|
55
|
-
p += 5; continue
|
|
56
|
-
}
|
|
57
|
-
if (n === 39) { fg = null; p++; continue }
|
|
58
|
-
if (n === 49) { bg = null; p++; continue }
|
|
59
|
-
p++
|
|
60
|
-
}
|
|
61
|
-
i = end + 1
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
// A cell — consume one codepoint (handles surrogate pairs)
|
|
65
|
-
const cp = line.codePointAt(i)!
|
|
66
|
-
const ch = String.fromCodePoint(cp)
|
|
67
|
-
i += ch.length
|
|
68
|
-
const efg = reverse ? bg : fg
|
|
69
|
-
const ebg = reverse ? fg : bg
|
|
70
|
-
out.push({ ch, fg: efg, bg: ebg })
|
|
71
|
-
}
|
|
72
|
-
return out
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Parse a multi-line chafa output into rows. */
|
|
76
|
-
export function parseChafa(text: string): Cell[][] {
|
|
77
|
-
return text.split("\n").filter(s => s.length > 0).map(parseChafaLine)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Hex color helper for OpenTUI fg/bg props. */
|
|
81
|
-
export function hex(c: RGB | null): string | undefined {
|
|
82
|
-
if (!c) return undefined
|
|
83
|
-
return `#${c.r.toString(16).padStart(2, "0")}${c.g.toString(16).padStart(2, "0")}${c.b.toString(16).padStart(2, "0")}`
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─── Rendering pipeline ─────────────────────────────────────────────────
|
|
87
|
-
//
|
|
88
|
-
// renderChafa() shells the chafa binary and returns parsed rows. Cached by
|
|
89
|
-
// (resolved path, mtime, width) — re-renders on the same file at the same
|
|
90
|
-
// width are free. Cache is an LRU capped at ~50 entries to bound memory
|
|
91
|
-
// for a scrollback with lots of images. Height is omitted from the key
|
|
92
|
-
// because we always pass chafa a 4:1 w/h cap and let it pick the actual
|
|
93
|
-
// row count to preserve aspect.
|
|
94
|
-
|
|
95
|
-
import { spawnSync } from "child_process"
|
|
96
|
-
import { existsSync, statSync } from "fs"
|
|
97
|
-
|
|
98
|
-
const CHAFA_PATHS = [
|
|
99
|
-
"/usr/sbin/chafa",
|
|
100
|
-
"/usr/bin/chafa",
|
|
101
|
-
"/usr/local/bin/chafa",
|
|
102
|
-
"/opt/homebrew/bin/chafa",
|
|
103
|
-
"/home/linuxbrew/.linuxbrew/bin/chafa",
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
let cachedBin: string | null | undefined = undefined
|
|
107
|
-
|
|
108
|
-
/** Locate the chafa binary once per process. null → not installed. */
|
|
109
|
-
export function chafaBin(): string | null {
|
|
110
|
-
if (cachedBin !== undefined) return cachedBin
|
|
111
|
-
for (const p of CHAFA_PATHS) if (existsSync(p)) { cachedBin = p; return p }
|
|
112
|
-
cachedBin = null
|
|
113
|
-
return null
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Expand ~ in a user-supplied path. Returns the absolute path or null
|
|
117
|
-
* if the file doesn't exist. */
|
|
118
|
-
export function resolveImage(path: string): string | null {
|
|
119
|
-
const full = path.startsWith("~")
|
|
120
|
-
? (process.env.HOME ?? "") + path.slice(1)
|
|
121
|
-
: path
|
|
122
|
-
return existsSync(full) ? full : null
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export type Rendered = { rows: Cell[][] } | { err: string }
|
|
126
|
-
|
|
127
|
-
const CACHE = new Map<string, Cell[][]>()
|
|
128
|
-
const CACHE_CAP = 50
|
|
129
|
-
|
|
130
|
-
function cacheGet(k: string): Cell[][] | undefined {
|
|
131
|
-
const v = CACHE.get(k)
|
|
132
|
-
if (!v) return undefined
|
|
133
|
-
// LRU touch: re-insert at tail
|
|
134
|
-
CACHE.delete(k)
|
|
135
|
-
CACHE.set(k, v)
|
|
136
|
-
return v
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function cachePut(k: string, v: Cell[][]): void {
|
|
140
|
-
if (CACHE.size >= CACHE_CAP) CACHE.delete(CACHE.keys().next().value!)
|
|
141
|
-
CACHE.set(k, v)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Render an image to parsed cells at the given cell-width. Height is
|
|
145
|
-
* capped at roughly width/3 so a 2:1-ish image fits most message widths.
|
|
146
|
-
* Returns { err } on any failure (caller should fall back to MediaChip). */
|
|
147
|
-
export function renderChafa(path: string, width: number, height?: number): Rendered {
|
|
148
|
-
const bin = chafaBin()
|
|
149
|
-
if (!bin) return { err: "chafa not installed" }
|
|
150
|
-
const full = resolveImage(path)
|
|
151
|
-
if (!full) return { err: `not found: ${path}` }
|
|
152
|
-
|
|
153
|
-
let mtime = 0
|
|
154
|
-
try { mtime = statSync(full).mtimeMs | 0 } catch { /* ignore — cache key is still unique */ }
|
|
155
|
-
const h = height ?? Math.max(6, Math.round(width / 3))
|
|
156
|
-
const key = `${full}:${mtime}:${width}x${h}`
|
|
157
|
-
const cached = cacheGet(key)
|
|
158
|
-
if (cached) return { rows: cached }
|
|
159
|
-
|
|
160
|
-
const r = spawnSync(bin, [
|
|
161
|
-
`--size=${width}x${h}`,
|
|
162
|
-
"--format=symbols",
|
|
163
|
-
"--symbols=block",
|
|
164
|
-
"--colors=full",
|
|
165
|
-
full,
|
|
166
|
-
], { encoding: "utf8", timeout: 5_000 })
|
|
167
|
-
if (r.error) return { err: r.error.message }
|
|
168
|
-
if (r.status !== 0) return { err: (r.stderr || `chafa exit ${r.status}`).trim() }
|
|
169
|
-
|
|
170
|
-
const rows = parseChafa(r.stdout)
|
|
171
|
-
cachePut(key, rows)
|
|
172
|
-
return { rows }
|
|
173
|
-
}
|
package/src/utils/clipboard.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { platform } from "os"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Writes text to clipboard via OSC 52 escape sequence.
|
|
5
|
-
* Works over SSH by having the terminal emulator handle it locally.
|
|
6
|
-
*/
|
|
7
|
-
function writeOsc52(text: string): void {
|
|
8
|
-
if (!process.stdout.isTTY) return
|
|
9
|
-
const base64 = Buffer.from(text).toString("base64")
|
|
10
|
-
const osc52 = `\x1b]52;c;${base64}\x07`
|
|
11
|
-
const pass = process.env["TMUX"] || process.env["STY"]
|
|
12
|
-
const seq = pass ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
|
|
13
|
-
process.stdout.write(seq)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function nativeCopy(text: string): Promise<void> {
|
|
17
|
-
const os = platform()
|
|
18
|
-
|
|
19
|
-
if (os === "darwin") {
|
|
20
|
-
const proc = Bun.spawn(["pbcopy"], { stdin: "pipe" })
|
|
21
|
-
proc.stdin.write(text)
|
|
22
|
-
proc.stdin.end()
|
|
23
|
-
await proc.exited
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (os === "linux") {
|
|
28
|
-
if (process.env["WAYLAND_DISPLAY"]) {
|
|
29
|
-
try {
|
|
30
|
-
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
|
31
|
-
proc.stdin.write(text)
|
|
32
|
-
proc.stdin.end()
|
|
33
|
-
await proc.exited
|
|
34
|
-
return
|
|
35
|
-
} catch {}
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
|
39
|
-
proc.stdin.write(text)
|
|
40
|
-
proc.stdin.end()
|
|
41
|
-
await proc.exited
|
|
42
|
-
return
|
|
43
|
-
} catch {}
|
|
44
|
-
try {
|
|
45
|
-
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
|
46
|
-
proc.stdin.write(text)
|
|
47
|
-
proc.stdin.end()
|
|
48
|
-
await proc.exited
|
|
49
|
-
return
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function copy(text: string): Promise<void> {
|
|
55
|
-
writeOsc52(text)
|
|
56
|
-
await nativeCopy(text).catch(() => {})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function copySelection(renderer: { getSelection: () => { getSelectedText: () => string } | null; clearSelection: () => void }): boolean {
|
|
60
|
-
const sel = renderer.getSelection()
|
|
61
|
-
const text = sel?.getSelectedText()
|
|
62
|
-
if (!text) return false
|
|
63
|
-
|
|
64
|
-
copy(text).catch(() => {})
|
|
65
|
-
renderer.clearSelection()
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* context-segments.ts — Parse the Hermes system prompt into sections,
|
|
3
|
-
* group them into a two-level hierarchy, and generate grids.
|
|
4
|
-
*
|
|
5
|
-
* Level 0 (top): System Prompt | System Tools | MCP Tools | Memory |
|
|
6
|
-
* Skills | Conversation | Free
|
|
7
|
-
* Level 1 (drill): Children of a group, e.g. Memory → SOUL | Notes |
|
|
8
|
-
* User Profile | Mem0 | Providers
|
|
9
|
-
*
|
|
10
|
-
* The grid always fills 256 cells. At top level, percentages are relative
|
|
11
|
-
* to the full context length. At drill level, relative to the parent group.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { Source, ToolInfo } from "./hermes-home"
|
|
15
|
-
import { makeSource } from "./hermes-home"
|
|
16
|
-
import { count as tok } from "./tokens"
|
|
17
|
-
|
|
18
|
-
// ─── Types ───────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
/** A parsed section of the system prompt */
|
|
21
|
-
export type Section = {
|
|
22
|
-
readonly id: string
|
|
23
|
-
readonly label: string
|
|
24
|
-
readonly chars: number
|
|
25
|
-
readonly tokens: number
|
|
26
|
-
readonly text: string
|
|
27
|
-
readonly source?: Source
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** A segment in the grid — leaf or group */
|
|
31
|
-
export type Segment = {
|
|
32
|
-
readonly id: string
|
|
33
|
-
readonly label: string
|
|
34
|
-
readonly tokens: number
|
|
35
|
-
readonly percent: number
|
|
36
|
-
readonly children?: ReadonlyArray<Segment>
|
|
37
|
-
readonly section?: Section
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Grid cell */
|
|
41
|
-
type Cell = { readonly id: string }
|
|
42
|
-
|
|
43
|
-
// ─── Constants ───────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
const GRID = 256
|
|
46
|
-
// Chars-per-token fallback for ToolInfo (description/param lengths are
|
|
47
|
-
// char counts; the text itself is not retained in the snapshot — so we
|
|
48
|
-
// can't run the real tokenizer until upstream ToolInfo carries text).
|
|
49
|
-
const CPT = 4
|
|
50
|
-
|
|
51
|
-
/** Parsed-section IDs that belong to the Memory top-level category */
|
|
52
|
-
const MEMORY_IDS = new Set(["soul", "memory", "user", "mem0"])
|
|
53
|
-
|
|
54
|
-
/** Parsed-section IDs that are residual system-prompt framing */
|
|
55
|
-
const SYSTEM_PROMPT_IDS = new Set(["project", "meta", "other"])
|
|
56
|
-
|
|
57
|
-
// ─── System Prompt Parser ────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/** Parse raw system prompt text into sections by structural delimiters */
|
|
60
|
-
export function parse(text: string): Section[] {
|
|
61
|
-
if (!text) return []
|
|
62
|
-
|
|
63
|
-
const sections: Section[] = []
|
|
64
|
-
const used = new Array(text.length).fill(false)
|
|
65
|
-
|
|
66
|
-
const mark = (start: number, end: number, id: string, label: string, source?: Source) => {
|
|
67
|
-
const slice = text.slice(start, end)
|
|
68
|
-
if (slice.trim().length === 0) return
|
|
69
|
-
for (let i = start; i < end; i++) used[i] = true
|
|
70
|
-
sections.push({ id, label, chars: slice.length, tokens: tok(slice), text: slice, source })
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// SOUL.md — start to first ══════
|
|
74
|
-
const bar1 = text.indexOf("══════")
|
|
75
|
-
if (bar1 > 0) mark(0, bar1, "soul", "SOUL.md", makeSource("SOUL.md"))
|
|
76
|
-
|
|
77
|
-
// MEMORY block
|
|
78
|
-
const memH = text.indexOf("MEMORY (your personal notes)")
|
|
79
|
-
if (memH >= 0) {
|
|
80
|
-
const s = text.lastIndexOf("══════", memH)
|
|
81
|
-
const after = text.indexOf("\n", text.indexOf("══════", memH + 1))
|
|
82
|
-
const next = text.indexOf("══════", after > 0 ? after : memH + 40)
|
|
83
|
-
const e = next > 0 ? text.lastIndexOf("\n", next) + 1 : text.length
|
|
84
|
-
if (s >= 0) mark(s, e, "memory", "Memory Notes", makeSource("memories/MEMORY.md", "MEMORY.md"))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// USER PROFILE block
|
|
88
|
-
const userH = text.indexOf("USER PROFILE (who the user is)")
|
|
89
|
-
if (userH >= 0) {
|
|
90
|
-
const s = text.lastIndexOf("══════", userH)
|
|
91
|
-
const after = text.indexOf("\n", text.indexOf("══════", userH + 1))
|
|
92
|
-
const rest = text.slice(after > 0 ? after : userH + 40)
|
|
93
|
-
const next = rest.search(/\n#\s/)
|
|
94
|
-
const e = next >= 0 ? (after > 0 ? after : userH + 40) + next + 1 : text.length
|
|
95
|
-
if (s >= 0) mark(s, e, "user", "User Profile", makeSource("memories/USER.md", "USER.md"))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Mem0 Memory
|
|
99
|
-
const m0 = text.indexOf("# Mem0 Memory")
|
|
100
|
-
if (m0 >= 0) {
|
|
101
|
-
const rest = text.slice(m0 + 1)
|
|
102
|
-
const next = rest.search(/\n##?\s/)
|
|
103
|
-
mark(m0, next >= 0 ? m0 + 1 + next + 1 : text.length, "mem0", "Mem0 Memory")
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Skills catalog
|
|
107
|
-
const skH = text.indexOf("## Skills (mandatory)")
|
|
108
|
-
const skE = text.indexOf("</available_skills>")
|
|
109
|
-
if (skH >= 0 && skE >= 0) {
|
|
110
|
-
let end = text.indexOf("\n", skE)
|
|
111
|
-
while (end < text.length && end >= 0) {
|
|
112
|
-
const nl = text.indexOf("\n", end + 1)
|
|
113
|
-
if (nl < 0) { end = text.length; break }
|
|
114
|
-
const line = text.slice(end + 1, nl).trim()
|
|
115
|
-
if (line.startsWith("#")) break
|
|
116
|
-
if (line === "") {
|
|
117
|
-
const peek = text.slice(nl + 1, text.indexOf("\n", nl + 1)).trim()
|
|
118
|
-
if (peek.startsWith("#")) { end = nl; break }
|
|
119
|
-
}
|
|
120
|
-
end = nl
|
|
121
|
-
}
|
|
122
|
-
mark(skH, end + 1, "skills", "Skills Catalog", makeSource("skills", "skills/"))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Project Context
|
|
126
|
-
const proj = text.indexOf("# Project Context")
|
|
127
|
-
if (proj >= 0) {
|
|
128
|
-
const conv = text.indexOf("Conversation started:")
|
|
129
|
-
mark(proj, conv > proj ? conv : text.length, "project", "Project Context", makeSource("AGENTS.md"))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Session metadata
|
|
133
|
-
const conv = text.indexOf("Conversation started:")
|
|
134
|
-
if (conv >= 0) mark(conv, text.length, "meta", "Session Metadata")
|
|
135
|
-
|
|
136
|
-
// Unmarked regions → "other"
|
|
137
|
-
let start = -1
|
|
138
|
-
for (let i = 0; i <= text.length; i++) {
|
|
139
|
-
if (i < text.length && !used[i]) {
|
|
140
|
-
if (start < 0) start = i
|
|
141
|
-
} else if (start >= 0) {
|
|
142
|
-
const slice = text.slice(start, i)
|
|
143
|
-
if (slice.trim().length > 0) {
|
|
144
|
-
sections.push({ id: "other", label: "Other", chars: slice.length, tokens: tok(slice), text: slice })
|
|
145
|
-
}
|
|
146
|
-
start = -1
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return sections.sort((a, b) => text.indexOf(a.text) - text.indexOf(b.text))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── Tool Classification ─────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
/** Classify tools by origin. MCP tools always have `mcp_` prefix — this is
|
|
156
|
-
* a guaranteed convention (mcp_tool.py:2394 collision guard). */
|
|
157
|
-
export function classifyTools(
|
|
158
|
-
tools: ReadonlyArray<ToolInfo>,
|
|
159
|
-
): { system: ToolInfo[]; mcp: ToolInfo[] } {
|
|
160
|
-
const system: ToolInfo[] = []
|
|
161
|
-
const mcp: ToolInfo[] = []
|
|
162
|
-
for (const t of tools) {
|
|
163
|
-
if (t.name.startsWith("mcp_")) mcp.push(t)
|
|
164
|
-
else system.push(t)
|
|
165
|
-
}
|
|
166
|
-
return { system, mcp }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Token estimate for a tool's schema entry. chars/4 fallback — ToolInfo
|
|
170
|
-
* only carries lengths, not text. */
|
|
171
|
-
export function toolTokens(tool: ToolInfo): number {
|
|
172
|
-
return Math.ceil((tool.descriptionLength + tool.paramsLength) / CPT)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ─── Segment Builder ─────────────────────────────────────────────────
|
|
176
|
-
|
|
177
|
-
type Opts = {
|
|
178
|
-
contextLength: number
|
|
179
|
-
inputTokens: number
|
|
180
|
-
sections: ReadonlyArray<Section>
|
|
181
|
-
conversationTokens: number
|
|
182
|
-
tools: ReadonlyArray<ToolInfo>
|
|
183
|
-
/** Optional — total tokens consumed by installed skills (name+description
|
|
184
|
-
* per skill). When absent, falls back to the "skills" section from parse()
|
|
185
|
-
* (which reflects what's actually injected into the system prompt). */
|
|
186
|
-
skillsTokens?: number
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Build the two-level segment hierarchy.
|
|
191
|
-
* Returns top-level groups. Groups with children can be drilled into.
|
|
192
|
-
*
|
|
193
|
-
* Top-level categories (in display order):
|
|
194
|
-
* system_prompt | system_tools | mcp_tools | memory | skills | conversation | free
|
|
195
|
-
*/
|
|
196
|
-
export function build(opts: Opts): Segment[] {
|
|
197
|
-
const pct = (t: number) => opts.contextLength > 0 ? (t / opts.contextLength) * 100 : 0
|
|
198
|
-
const result: Segment[] = []
|
|
199
|
-
|
|
200
|
-
const byId = new Map<string, Section>()
|
|
201
|
-
for (const s of opts.sections) byId.set(s.id, s)
|
|
202
|
-
|
|
203
|
-
// ── 1. System Prompt (framing only — project + meta + other) ──
|
|
204
|
-
const promptChildren: Segment[] = opts.sections
|
|
205
|
-
.filter(sec => SYSTEM_PROMPT_IDS.has(sec.id) && sec.tokens > 0)
|
|
206
|
-
.map(sec => ({
|
|
207
|
-
id: sec.id,
|
|
208
|
-
label: sec.label,
|
|
209
|
-
tokens: sec.tokens,
|
|
210
|
-
percent: pct(sec.tokens),
|
|
211
|
-
section: sec,
|
|
212
|
-
}))
|
|
213
|
-
const promptTok = promptChildren.reduce((s, c) => s + c.tokens, 0)
|
|
214
|
-
if (promptTok > 0) {
|
|
215
|
-
result.push({
|
|
216
|
-
id: "system_prompt",
|
|
217
|
-
label: "System Prompt",
|
|
218
|
-
tokens: promptTok,
|
|
219
|
-
percent: pct(promptTok),
|
|
220
|
-
children: promptChildren,
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ── 2. System Tools (non-MCP) ──
|
|
225
|
-
const { system: sysTools, mcp: mcpTools } = classifyTools(opts.tools)
|
|
226
|
-
const sysToolsTok = sysTools.reduce((s, t) => s + toolTokens(t), 0)
|
|
227
|
-
if (sysToolsTok > 0) {
|
|
228
|
-
result.push({
|
|
229
|
-
id: "system_tools",
|
|
230
|
-
label: "System Tools",
|
|
231
|
-
tokens: sysToolsTok,
|
|
232
|
-
percent: pct(sysToolsTok),
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── 3. MCP Tools ──
|
|
237
|
-
const mcpToolsTok = mcpTools.reduce((s, t) => s + toolTokens(t), 0)
|
|
238
|
-
if (mcpToolsTok > 0) {
|
|
239
|
-
result.push({
|
|
240
|
-
id: "mcp_tools",
|
|
241
|
-
label: "MCP Tools",
|
|
242
|
-
tokens: mcpToolsTok,
|
|
243
|
-
percent: pct(mcpToolsTok),
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ── 4. Memory (SOUL + Notes + Profile + Mem0) ──
|
|
248
|
-
const memChildren: Segment[] = opts.sections
|
|
249
|
-
.filter(sec => MEMORY_IDS.has(sec.id) && sec.tokens > 0)
|
|
250
|
-
.map(sec => ({
|
|
251
|
-
id: sec.id,
|
|
252
|
-
label: sec.label,
|
|
253
|
-
tokens: sec.tokens,
|
|
254
|
-
percent: pct(sec.tokens),
|
|
255
|
-
section: sec,
|
|
256
|
-
}))
|
|
257
|
-
const memTok = memChildren.reduce((s, c) => s + c.tokens, 0)
|
|
258
|
-
if (memTok > 0) {
|
|
259
|
-
result.push({
|
|
260
|
-
id: "memory",
|
|
261
|
-
label: "Memory",
|
|
262
|
-
tokens: memTok,
|
|
263
|
-
percent: pct(memTok),
|
|
264
|
-
children: memChildren,
|
|
265
|
-
})
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ── 5. Skills (catalog injected into system prompt) ──
|
|
269
|
-
const skillsSec = byId.get("skills")
|
|
270
|
-
const skillsTok = skillsSec?.tokens ?? opts.skillsTokens ?? 0
|
|
271
|
-
if (skillsTok > 0) {
|
|
272
|
-
result.push({
|
|
273
|
-
id: "skills",
|
|
274
|
-
label: "Skills",
|
|
275
|
-
tokens: skillsTok,
|
|
276
|
-
percent: pct(skillsTok),
|
|
277
|
-
section: skillsSec,
|
|
278
|
-
})
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ── 6. Conversation ──
|
|
282
|
-
if (opts.conversationTokens > 0) {
|
|
283
|
-
const ct = Math.min(opts.conversationTokens, opts.inputTokens || opts.conversationTokens)
|
|
284
|
-
result.push({ id: "conversation", label: "Conversation", tokens: ct, percent: pct(ct) })
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ── 7. Free ──
|
|
288
|
-
const taken = result.reduce((s, g) => s + g.tokens, 0)
|
|
289
|
-
const free = Math.max(0, opts.contextLength - taken)
|
|
290
|
-
result.push({ id: "free", label: "Free", tokens: free, percent: pct(free) })
|
|
291
|
-
|
|
292
|
-
return result
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Get drilled-in segments for a group, with percentages rescaled
|
|
297
|
-
* to fill the grid relative to the group's total.
|
|
298
|
-
*/
|
|
299
|
-
export function drill(group: Segment): Segment[] {
|
|
300
|
-
if (!group.children || group.children.length === 0) return []
|
|
301
|
-
const total = group.tokens
|
|
302
|
-
return group.children.map(c => ({
|
|
303
|
-
...c,
|
|
304
|
-
percent: total > 0 ? (c.tokens / total) * 100 : 0,
|
|
305
|
-
}))
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ─── Grid Generator ──────────────────────────────────────────────────
|
|
309
|
-
|
|
310
|
-
/** Generate 256 cells from segments, proportional to percent */
|
|
311
|
-
export function cells(segments: ReadonlyArray<Segment>, fallback = "free"): Cell[] {
|
|
312
|
-
const filled = segments.flatMap(seg =>
|
|
313
|
-
Array.from({ length: Math.round((seg.percent / 100) * GRID) }, () => ({ id: seg.id }))
|
|
314
|
-
)
|
|
315
|
-
const pad = Array.from({ length: Math.max(0, GRID - filled.length) }, () => ({ id: fallback }))
|
|
316
|
-
return [...filled, ...pad].slice(0, GRID)
|
|
317
|
-
}
|