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,190 +0,0 @@
1
- // Checkpoint rollback browser — list git-style checkpoints the gateway
2
- // has captured, preview a unified diff, and restore on confirm.
3
- //
4
- // State lives entirely inside <RollbackDialog>; the global dialog
5
- // provider closes on Esc, so when we want Esc to mean "back to list"
6
- // from the diff view we immediately re-replace() ourselves with the
7
- // already-loaded data (our key handler registers after the provider's,
8
- // so our replace wins the batched setState race).
9
-
10
- import { useEffect, useState } from "react"
11
- import { useKeyboard } from "@opentui/react"
12
- import { useKeys, handleListKey } from "../keys"
13
- import { useTheme } from "../theme"
14
- import type { DialogContext } from "../ui/dialog"
15
- import type { useToast } from "../ui/toast"
16
- import type { Gateway } from "../app/gateway"
17
- import { DiffBlock } from "../components/chat/DiffBlock"
18
- import { ago, trunc } from "../ui/fmt"
19
-
20
- type Toast = ReturnType<typeof useToast>
21
-
22
- type Checkpoint = { hash: string; timestamp: number; message: string }
23
- type ListRes = { enabled: boolean; checkpoints: Checkpoint[] }
24
- type DiffRes = { stat: string; diff: string; rendered?: string }
25
- type RestoreRes = { success: boolean; history_removed?: number }
26
-
27
- type Props = {
28
- gw: Gateway
29
- toast: Toast
30
- dialog: DialogContext
31
- /** Pre-loaded list (used when Esc re-opens at list view). */
32
- initial?: ListRes
33
- sel?: number
34
- }
35
-
36
- const RollbackDialog = (props: Props) => {
37
- const theme = useTheme().theme
38
- const [data, setData] = useState<ListRes | null>(props.initial ?? null)
39
- const [sel, setSel] = useState(props.sel ?? 0)
40
- const [diff, setDiff] = useState<DiffRes | null>(null)
41
- const [confirm, setConfirm] = useState(false)
42
-
43
- useEffect(() => {
44
- if (props.initial) return
45
- props.gw.request<ListRes>("rollback.list")
46
- .then(setData)
47
- .catch((e: Error) => setData({ enabled: false, checkpoints: [], ...{ err: e.message } } as ListRes))
48
- }, [props.gw, props.initial])
49
-
50
- const points = data?.checkpoints ?? []
51
- const cur = points[sel]
52
-
53
- const open = (cp: Checkpoint) => {
54
- props.gw.request<DiffRes>("rollback.diff", { hash: cp.hash })
55
- .then(setDiff)
56
- .catch((e: Error) => props.toast.error(e))
57
- }
58
-
59
- const back = () => {
60
- setDiff(null)
61
- setConfirm(false)
62
- // Provider already dispatched clear() on this Esc — replace() wins
63
- // the batch. React reconciles same-type at same slot and keeps our
64
- // state, so the setDiff(null) above is what actually flips the view.
65
- props.dialog.replace(
66
- <RollbackDialog gw={props.gw} toast={props.toast} dialog={props.dialog}
67
- initial={data ?? undefined} sel={sel} />,
68
- )
69
- }
70
-
71
- const restore = (cp: Checkpoint) => {
72
- props.gw.request<RestoreRes>("rollback.restore", { hash: cp.hash })
73
- .then(r => {
74
- if (!r.success) throw new Error("restore rejected")
75
- const n = r.history_removed
76
- props.toast.show({ variant: "success",
77
- message: `Restored ${cp.hash.slice(0, 7)}${n ? ` · ${n} turns removed` : ""}` })
78
- props.dialog.clear()
79
- })
80
- .catch((e: Error) => {
81
- props.toast.show({ variant: "error", message: `Restore failed: ${e.message}` })
82
- props.dialog.clear()
83
- })
84
- }
85
-
86
- const keys = useKeys()
87
- useKeyboard((key) => {
88
- if (diff) {
89
- if (confirm) {
90
- if (keys.match("dialog.confirm", key)) return restore(cur)
91
- if (keys.match("dialog.deny", key) || keys.match("dialog.cancel", key)) {
92
- setConfirm(false); return back()
93
- }
94
- return
95
- }
96
- if (keys.match("dialog.cancel", key)) return back()
97
- if (key.name === "r") return setConfirm(true)
98
- return
99
- }
100
- if (!data?.enabled) return
101
- handleListKey(keys, key, {
102
- count: points.length, setSel,
103
- onActivate: () => { if (cur) open(cur) },
104
- })
105
- })
106
-
107
- // ── Render ────────────────────────────────────────────────────────
108
-
109
- if (!data) return (
110
- <box width={60} height={3}>
111
- <text fg={theme.textMuted}>Loading checkpoints…</text>
112
- </box>
113
- )
114
-
115
- if (!data.enabled) return (
116
- <box flexDirection="column" width={60} height={5}>
117
- <box height={1}><text fg={theme.warning}><strong>Checkpoints disabled</strong></text></box>
118
- <box height={1} />
119
- <box height={1}><text fg={theme.textMuted}>Enable checkpoints in config to use /rollback.</text></box>
120
- <box height={1} />
121
- <box height={1}><text fg={theme.textMuted}>Esc to close</text></box>
122
- </box>
123
- )
124
-
125
- if (diff) {
126
- const body = diff.rendered || diff.diff || diff.stat || "(empty diff)"
127
- return (
128
- <box flexDirection="column" width={110} height={30}>
129
- <box height={1}><text>
130
- <span fg={theme.primary}><strong>Rollback · </strong></span>
131
- <span fg={theme.accent}>{cur.hash.slice(0, 7)}</span>
132
- <span fg={theme.textMuted}>{` ${trunc(cur.message, 70)}`}</span>
133
- </text></box>
134
- <box height={1}><text fg={theme.textMuted}>{diff.stat || " "}</text></box>
135
- <box height={1} />
136
- <scrollbox scrollY flexGrow={1}>
137
- <box flexDirection="column" width="100%">
138
- <DiffBlock text={body} />
139
- </box>
140
- </scrollbox>
141
- <box height={1} />
142
- {confirm ? (
143
- <box height={1}><text>
144
- <span fg={theme.warning}><strong>Restore this checkpoint? </strong></span>
145
- <span fg={theme.textMuted}>[y] restore [n] cancel</span>
146
- </text></box>
147
- ) : (
148
- <box height={1}><text fg={theme.textMuted}>[r] restore · Esc back</text></box>
149
- )}
150
- </box>
151
- )
152
- }
153
-
154
- return (
155
- <box flexDirection="column" width={90} height={Math.min(28, Math.max(8, points.length + 6))}>
156
- <box height={1}><text fg={theme.primary}><strong>Rollback</strong></text></box>
157
- <box height={1}><text fg={theme.textMuted}>
158
- {`${points.length} checkpoints · ↑↓ navigate Enter diff Esc close`}
159
- </text></box>
160
- <box height={1} />
161
- {points.length === 0 ? (
162
- <box height={1}><text fg={theme.textMuted}>No checkpoints yet.</text></box>
163
- ) : (
164
- <scrollbox scrollY flexGrow={1}>
165
- <box flexDirection="column" width="100%">
166
- {points.map((cp, i) => {
167
- const on = i === sel
168
- return (
169
- <box key={cp.hash} height={1}
170
- backgroundColor={on ? theme.backgroundElement : undefined}
171
- onMouseDown={() => { setSel(i); open(cp) }}
172
- onMouseOver={() => setSel(i)}>
173
- <text>
174
- <span fg={on ? theme.primary : theme.textMuted}>{on ? "▸ " : " "}</span>
175
- <span fg={theme.accent}>{cp.hash.slice(0, 7).padEnd(9)}</span>
176
- <span fg={theme.textMuted}>{ago(cp.timestamp).padEnd(12)}</span>
177
- <span fg={on ? theme.text : theme.textMuted}>{trunc(cp.message, 56)}</span>
178
- </text>
179
- </box>
180
- )
181
- })}
182
- </box>
183
- </scrollbox>
184
- )}
185
- </box>
186
- )
187
- }
188
-
189
- export const openRollback = (dialog: DialogContext, gw: Gateway, toast: Toast) =>
190
- dialog.replace(<RollbackDialog gw={gw} toast={toast} dialog={dialog} />)
@@ -1,80 +0,0 @@
1
- import type { Gateway } from "../app/gateway"
2
- import type { DialogContext } from "../ui/dialog"
3
- import { DialogSelect } from "../ui/dialog-select"
4
- import type { SpawnTreeEntry, SpawnTreeSnapshot, SpawnSubagent } from "../utils/gateway-types"
5
- import { useTheme } from "../theme"
6
- import { dur, when, fmt, trunc } from "../ui/fmt"
7
-
8
- // Browse persisted spawn trees (spawn_tree.list) and inspect one
9
- // (spawn_tree.load). Read-only — the live tree lives in the Agents tab.
10
-
11
- const Status = ({ s }: { s: SpawnSubagent["status"] }) => {
12
- const theme = useTheme().theme
13
- const fg = s === "completed" ? theme.success
14
- : s === "failed" ? theme.error
15
- : s === "interrupted" ? theme.warning
16
- : theme.textMuted
17
- return <span fg={fg}>{s}</span>
18
- }
19
-
20
- const SnapshotView = (props: { entry: SpawnTreeEntry; snap: SpawnTreeSnapshot }) => {
21
- const theme = useTheme().theme
22
- const subs = props.snap.subagents ?? []
23
- const tok = subs.reduce((n, s) => n + (s.input_tokens ?? 0) + (s.output_tokens ?? 0), 0)
24
- const span = props.snap.started_at && props.snap.finished_at
25
- ? dur(props.snap.finished_at - props.snap.started_at) : "—"
26
- return (
27
- <box flexDirection="column" width={80}>
28
- <text fg={theme.text}><strong>{props.entry.label || `${subs.length} subagents`}</strong></text>
29
- <text fg={theme.textMuted}>{when(props.entry.finished_at)} · {span} · {subs.length} agents · {fmt(tok)} tok</text>
30
- <box height={1} />
31
- <scrollbox scrollY maxHeight={20} contentOptions={{ flexDirection: "column" }}>
32
- {subs.map(s => (
33
- <box key={s.subagent_id} flexDirection="column" marginBottom={1}>
34
- <box height={1}>
35
- <text>
36
- <span fg={theme.textMuted}>{"┃ " + "· ".repeat(s.depth)}</span>
37
- <span fg={theme.text}>{trunc(s.goal.replace(/\s+/g, " "), 60)}</span>
38
- </text>
39
- </box>
40
- <box height={1}>
41
- <text fg={theme.textMuted}>
42
- {"┃ " + " ".repeat(2 * s.depth + 2)}
43
- <Status s={s.status} />
44
- {` · ${s.tool_count}t`}
45
- {s.finished_at ? ` · ${dur(s.finished_at - s.started_at)}` : ""}
46
- {s.model ? ` · ${s.model}` : ""}
47
- </text>
48
- </box>
49
- </box>
50
- ))}
51
- </scrollbox>
52
- </box>
53
- )
54
- }
55
-
56
- export function openSpawnHistory(dialog: DialogContext, gw: Gateway, sessionId: string): void {
57
- gw.request<{ entries: SpawnTreeEntry[] }>("spawn_tree.list", { session_id: sessionId, limit: 50 })
58
- .then(r => {
59
- const entries = r.entries ?? []
60
- dialog.replace(
61
- <DialogSelect
62
- title="Spawn history"
63
- placeholder={entries.length ? "filter…" : "no saved spawn trees"}
64
- options={entries.map(e => ({
65
- value: e.path,
66
- title: `${e.count.toString().padStart(2)}× ${trunc(e.label || "(unlabeled)", 40)}`,
67
- description: when(e.finished_at),
68
- category: e.session_id === sessionId ? "This session" : e.session_id,
69
- }))}
70
- onSelect={opt => {
71
- const entry = entries.find(e => e.path === opt.value)!
72
- gw.request<SpawnTreeSnapshot>("spawn_tree.load", { path: entry.path })
73
- .then(snap => dialog.replace(<SnapshotView entry={entry} snap={snap} />))
74
- .catch(() => dialog.clear())
75
- }}
76
- />,
77
- )
78
- })
79
- .catch(() => dialog.clear())
80
- }
@@ -1,68 +0,0 @@
1
- // Single-line text prompt dialog. Enter submits, Esc cancels.
2
-
3
- import { useState } from "react"
4
- import { useKeyboard } from "@opentui/react"
5
- import { useTheme } from "../theme"
6
- import type { DialogContext } from "../ui/dialog"
7
-
8
- type Props = {
9
- title: string
10
- label?: string
11
- initial?: string
12
- onSubmit: (value: string) => void
13
- onCancel: () => void
14
- }
15
-
16
- const TextPrompt = (props: Props) => {
17
- const theme = useTheme().theme
18
- const [value, setValue] = useState(props.initial ?? "")
19
-
20
- useKeyboard((key) => {
21
- if (key.name === "escape") return props.onCancel()
22
- if (key.name === "return") {
23
- const v = value.trim()
24
- if (v) props.onSubmit(v)
25
- return
26
- }
27
- if (key.name === "backspace") return setValue(v => v.slice(0, -1))
28
- if (key.ctrl && key.name === "u") return setValue("")
29
- if (!key.ctrl && !key.meta && key.raw && key.raw.length === 1 && key.raw >= " ")
30
- return setValue(v => v + key.raw)
31
- })
32
-
33
- return (
34
- <box flexDirection="column" width={60}>
35
- <box height={1}><text fg={theme.primary}><strong>{props.title}</strong></text></box>
36
- <box height={1} />
37
- {props.label ? <box height={1}><text fg={theme.textMuted}>{props.label}</text></box> : null}
38
- <box height={1} flexDirection="row" overflow="hidden">
39
- <box flexShrink={0}><text fg={theme.accent}>{"┃ "}</text></box>
40
- <box flexGrow={1} minWidth={0} height={1} overflow="hidden">
41
- <text>
42
- <span fg={theme.text}>{value}</span>
43
- <span fg={theme.accent}>█</span>
44
- </text>
45
- </box>
46
- </box>
47
- <box height={1} />
48
- <box height={1}><text fg={theme.textMuted}>
49
- {value.trim() ? "Enter confirm · Esc cancel · Ctrl+U clear" : "Esc cancel"}
50
- </text></box>
51
- </box>
52
- )
53
- }
54
-
55
- export function openTextPrompt(
56
- dialog: DialogContext,
57
- opts: { title: string; label?: string; initial?: string },
58
- ): Promise<string | null> {
59
- return new Promise((resolve) => {
60
- dialog.replace(
61
- <TextPrompt
62
- title={opts.title} label={opts.label} initial={opts.initial}
63
- onSubmit={(v) => { dialog.clear(); resolve(v) }}
64
- onCancel={() => { dialog.clear(); resolve(null) }}
65
- />,
66
- )
67
- })
68
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Theme picker dialog — live preview with DialogSelect.
3
- */
4
-
5
- import { useCallback } from "react"
6
- import { useTheme } from "../theme"
7
- import { useDialog } from "../ui/dialog"
8
- import { DialogSelect } from "../ui/dialog-select"
9
- import type { SelectOption } from "../ui/dialog-select"
10
-
11
- const ThemePickerDialog = ({ onConfirm }: { onConfirm: () => void }) => {
12
- const ctx = useTheme()
13
- const dialog = useDialog()
14
-
15
- const options: SelectOption[] = ctx.names.map(n => ({
16
- title: n,
17
- value: n,
18
- }))
19
-
20
- const onMove = useCallback((opt: SelectOption) => {
21
- ctx.set(opt.value)
22
- }, [ctx])
23
-
24
- const onSelect = useCallback((opt: SelectOption) => {
25
- ctx.set(opt.value)
26
- onConfirm()
27
- dialog.clear()
28
- }, [ctx, dialog, onConfirm])
29
-
30
- return (
31
- <DialogSelect
32
- title="Switch Theme"
33
- options={options}
34
- current={ctx.name}
35
- onSelect={onSelect}
36
- onMove={onMove}
37
- placeholder="Search themes..."
38
- />
39
- )
40
- }
41
-
42
- /** Open the theme picker, reverting on close without selection */
43
- export const openThemePicker = (dialog: ReturnType<typeof useDialog>, ctx: ReturnType<typeof useTheme>) => {
44
- const saved = ctx.name
45
- let confirmed = false
46
- dialog.replace(
47
- <ThemePickerDialog onConfirm={() => { confirmed = true }} />,
48
- () => { if (!confirmed) ctx.set(saved) }
49
- )
50
- }
package/src/home/index.ts DELETED
@@ -1,23 +0,0 @@
1
- import { useEffect, useSyncExternalStore } from "react"
2
- import { home, type HomeState, type SliceKey } from "./store"
3
-
4
- export { home } from "./store"
5
- export type { HomeState, SliceKey } from "./store"
6
-
7
- /**
8
- * Subscribe to a single slice of ~/.hermes/ state.
9
- *
10
- * Returns `undefined` until the first read resolves, then the current value.
11
- * Rerenders on fs.watch-driven invalidation or explicit `home.invalidate(k)`,
12
- * and only for the requested key.
13
- */
14
- export function useHome<K extends SliceKey>(k: K): HomeState[K] | undefined {
15
- const v = useSyncExternalStore(
16
- (cb) => home.subscribe(k, cb),
17
- () => home.get(k),
18
- )
19
- useEffect(() => {
20
- void home.ensure(k)
21
- }, [k])
22
- return v
23
- }
package/src/home/store.ts DELETED
@@ -1,267 +0,0 @@
1
- /**
2
- * Reactive data layer for ~/.hermes/.
3
- *
4
- * A slice is a typed reader with optional dependency keys and optional
5
- * fs.watch paths. `ensure(k)` lazily resolves deps, reads, and starts a
6
- * watcher on first pull. `invalidate(k)` drops the value, re-reads if there
7
- * are subscribers, and cascades to dependents.
8
- *
9
- * Slices registered below; remaining readers migrate in from
10
- * utils/hermes-home.ts as they're converted to collectors.
11
- */
12
-
13
- import { watch, existsSync, statSync, type FSWatcher } from "node:fs"
14
- import { dirname, basename } from "node:path"
15
- import {
16
- hermesPath,
17
- readConfig,
18
- readMemoryFile,
19
- readMemoryProviders,
20
- readEnvFile,
21
- readSoul,
22
- readLiveSessions,
23
- readSystemPromptInfo,
24
- readToolsFromLatestSession,
25
- queryRecentSessions,
26
- readSkillUsage,
27
- readCuratorState,
28
- type HermesConfig,
29
- type MemoryFileInfo,
30
- type MemoryProviderInfo,
31
- type SoulInfo,
32
- type LiveSession,
33
- type SystemPromptInfo,
34
- type ToolsInfo,
35
- type SessionRow,
36
- type SkillUsage,
37
- type CuratorState,
38
- } from "../utils/hermes-home"
39
- import { readMemoryActivity, type MemoryActivity } from "../utils/memory-activity"
40
-
41
- // ─── State shape ──────────────────────────────────────────────────────
42
-
43
- export interface HomeState {
44
- config: HermesConfig | null
45
- memory: MemoryFileInfo | null
46
- userProfile: MemoryFileInfo | null
47
- memoryProviders: MemoryProviderInfo[]
48
- memoryActivity: MemoryActivity[]
49
- env: Record<string, string>
50
- soul: SoulInfo | null
51
- liveSessions: Record<string, LiveSession>
52
- recentSessions: SessionRow[]
53
- systemPrompt: SystemPromptInfo | null
54
- toolsInfo: ToolsInfo | null
55
- skillUsage: Record<string, SkillUsage>
56
- curatorState: CuratorState | null
57
- }
58
-
59
- export type SliceKey = keyof HomeState
60
-
61
- export interface Slice<K extends SliceKey> {
62
- /** Produce the value. Receives already-resolved dependency values. */
63
- read: (deps: Partial<HomeState>) => Promise<HomeState[K]>
64
- /** Slice keys this reader needs resolved first. Invalidation cascades along these edges. */
65
- deps?: readonly SliceKey[]
66
- /** Absolute paths to fs.watch. Change → invalidate(k). */
67
- watch?: readonly string[]
68
- }
69
-
70
- type Slices = { [K in SliceKey]: Slice<K> }
71
-
72
- const SLICES: Slices = {
73
- config: {
74
- read: () => readConfig(),
75
- watch: [hermesPath("config.yaml")],
76
- },
77
- memory: {
78
- read: (d) => readMemoryFile("MEMORY.md", d.config?.memory?.memory_char_limit ?? 2200),
79
- deps: ["config"],
80
- watch: [hermesPath("memories/MEMORY.md")],
81
- },
82
- userProfile: {
83
- read: (d) => readMemoryFile("USER.md", d.config?.memory?.user_char_limit ?? 1375),
84
- deps: ["config"],
85
- watch: [hermesPath("memories/USER.md")],
86
- },
87
- memoryProviders: {
88
- read: (d) => readMemoryProviders(d.config?.memory?.provider ?? ""),
89
- deps: ["config"],
90
- },
91
- memoryActivity: {
92
- read: async () => readMemoryActivity(),
93
- },
94
- env: {
95
- read: () => readEnvFile(),
96
- watch: [hermesPath(".env")],
97
- },
98
- soul: {
99
- read: () => readSoul(),
100
- watch: [hermesPath("SOUL.md")],
101
- },
102
- liveSessions: {
103
- read: () => readLiveSessions(),
104
- watch: [hermesPath("sessions/sessions.json")],
105
- },
106
- // DB-backed slices are pull-only: WAL mode means writes land in
107
- // state.db-wal, so watching state.db misses them until checkpoint;
108
- // and the query is heavy enough that firing on every checkpoint from
109
- // an always-mounted Sidebar is wasteful. Consumers invalidate on
110
- // demand (section-open, `r`, post-mutation).
111
- recentSessions: {
112
- read: async () => queryRecentSessions(),
113
- },
114
- systemPrompt: {
115
- read: async () => readSystemPromptInfo(),
116
- },
117
- toolsInfo: {
118
- // Scans sessions/ for newest session_*.json — watching the dir picks
119
- // up new files without tracking a specific one.
120
- read: () => readToolsFromLatestSession(),
121
- watch: [hermesPath("sessions")],
122
- },
123
- skillUsage: {
124
- read: () => readSkillUsage(),
125
- watch: [hermesPath("skills/.usage.json")],
126
- },
127
- curatorState: {
128
- read: () => readCuratorState(),
129
- watch: [hermesPath("skills/.curator_state")],
130
- },
131
- }
132
-
133
- /** Reverse dep edges: key → slices that listed it in `deps`. */
134
- const DEPENDENTS: ReadonlyMap<SliceKey, readonly SliceKey[]> = (() => {
135
- const m = new Map<SliceKey, SliceKey[]>()
136
- for (const [k, s] of Object.entries(SLICES) as [SliceKey, Slice<SliceKey>][]) {
137
- for (const d of s.deps ?? []) {
138
- const arr = m.get(d) ?? []
139
- arr.push(k)
140
- m.set(d, arr)
141
- }
142
- }
143
- return m
144
- })()
145
-
146
- // ─── Store ────────────────────────────────────────────────────────────
147
-
148
- const DEBOUNCE_MS = 50
149
-
150
- export class HomeStore {
151
- private data: Partial<HomeState> = {}
152
- private subs = new Map<SliceKey, Set<() => void>>()
153
- private inflight = new Map<SliceKey, Promise<unknown>>()
154
- private watchers = new Map<SliceKey, FSWatcher[]>()
155
- private debounce = new Map<SliceKey, ReturnType<typeof setTimeout>>()
156
-
157
- /** Current value, or undefined if not yet loaded. Stable ref until changed. */
158
- get<K extends SliceKey>(k: K): HomeState[K] | undefined {
159
- return this.data[k] as HomeState[K] | undefined
160
- }
161
-
162
- /** Register a change listener. Returns unsubscribe. */
163
- subscribe<K extends SliceKey>(k: K, cb: () => void): () => void {
164
- let set = this.subs.get(k)
165
- if (!set) this.subs.set(k, (set = new Set()))
166
- set.add(cb)
167
- return () => set.delete(cb)
168
- }
169
-
170
- /**
171
- * Resolve deps, read, store, start watching (first call only), notify.
172
- * Concurrent calls for the same key share one inflight promise.
173
- */
174
- ensure<K extends SliceKey>(k: K): Promise<HomeState[K]> {
175
- if (k in this.data) return Promise.resolve(this.data[k] as HomeState[K])
176
- const hit = this.inflight.get(k)
177
- if (hit) return hit as Promise<HomeState[K]>
178
-
179
- const slice = SLICES[k]
180
- const p = (async () => {
181
- const deps: Partial<HomeState> = {}
182
- for (const d of slice.deps ?? []) {
183
- (deps as Record<SliceKey, unknown>)[d] = await this.ensure(d)
184
- }
185
- const v = await slice.read(deps)
186
- this.data[k] = v
187
- this.startWatch(k, slice.watch)
188
- this.notify(k)
189
- return v
190
- })().finally(() => this.inflight.delete(k))
191
-
192
- this.inflight.set(k, p)
193
- return p
194
- }
195
-
196
- /**
197
- * Drop the cached value; re-read if there are subscribers. Cascades to
198
- * dependents. Call after TUI-driven writes so the UI reflects the change
199
- * without waiting on a watch event.
200
- */
201
- invalidate(k: SliceKey): void {
202
- if (!(k in this.data) && !this.inflight.has(k)) return
203
- delete this.data[k]
204
- if (this.subs.get(k)?.size) void this.ensure(k)
205
- for (const dep of DEPENDENTS.get(k) ?? []) this.invalidate(dep)
206
- }
207
-
208
- /** Dispose all watchers and timers. Tests must call this. */
209
- close(): void {
210
- for (const ws of this.watchers.values()) for (const w of ws) w.close()
211
- for (const t of this.debounce.values()) clearTimeout(t)
212
- this.watchers.clear()
213
- this.debounce.clear()
214
- this.subs.clear()
215
- this.inflight.clear()
216
- this.data = {}
217
- }
218
-
219
- private notify(k: SliceKey): void {
220
- const set = this.subs.get(k)
221
- if (set) for (const cb of set) cb()
222
- }
223
-
224
- private startWatch(k: SliceKey, paths: readonly string[] | undefined): void {
225
- if (!paths || this.watchers.has(k)) return
226
- const ws: FSWatcher[] = []
227
- const fire = () => {
228
- const prev = this.debounce.get(k)
229
- if (prev) clearTimeout(prev)
230
- this.debounce.set(k, setTimeout(() => this.invalidate(k), DEBOUNCE_MS))
231
- }
232
- for (const p of paths) {
233
- // Watching a file by path is unreliable across rewrites on Linux
234
- // (inotify binds to the inode). Watch the parent dir and filter on
235
- // basename so rename-into-place editors still fire. Only watch the
236
- // path itself when it's an existing directory.
237
- //
238
- // Known runtime gap: Bun's fs.watch(dir) never emits for entries
239
- // created after the watcher was armed (Node does). A target that
240
- // didn't exist at first ensure stays non-reactive for the process
241
- // lifetime. In practice only an optional file absent at launch is
242
- // affected; in-TUI writes call invalidate() which re-reads directly,
243
- // and `r` forces a manual refresh for the rare external-edit case.
244
- let dir = dirname(p)
245
- let name: string | null = basename(p)
246
- try {
247
- if (existsSync(p) && statSync(p).isDirectory()) {
248
- dir = p
249
- name = null
250
- }
251
- } catch {
252
- continue
253
- }
254
- if (!existsSync(dir)) continue
255
- try {
256
- ws.push(watch(dir, { persistent: false }, (_ev, f) => {
257
- if (name === null || f === name) fire()
258
- }))
259
- } catch {
260
- // Unwatchable (permissions, exotic fs). Slice still works; just not reactive.
261
- }
262
- }
263
- this.watchers.set(k, ws)
264
- }
265
- }
266
-
267
- export const home = new HomeStore()