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/dialogs/rollback.tsx
DELETED
|
@@ -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()
|