herm-tui 1.0.0-dev.1 → 1.0.0-dev.11
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/README.md +8 -4
- package/assets/eikons/ares.eikon +367 -0
- package/assets/eikons/default.eikon +398 -0
- package/assets/eikons/mono.eikon +395 -0
- 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 +4151 -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/ui/command.tsx
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command palette — registry of named commands, each optionally bound
|
|
3
|
-
* to a catalog ActionId. The palette (palette.open) is just one way to
|
|
4
|
-
* reach them; any command with an `action` also fires when that chord
|
|
5
|
-
* is pressed, so the registry doubles as the dispatch table for global
|
|
6
|
-
* actions that don't need useAppKeys' composer/renderer state.
|
|
7
|
-
*
|
|
8
|
-
* const cmd = useCommand()
|
|
9
|
-
* useEffect(() => cmd.register([
|
|
10
|
-
* { title: "Help", value: "help", action: "help.open", onSelect: ... },
|
|
11
|
-
* ]), [])
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createContext, useContext, useState, useCallback, useRef, useMemo } from "react"
|
|
15
|
-
import type { ReactNode } from "react"
|
|
16
|
-
import { useKeyboard } from "@opentui/react"
|
|
17
|
-
import { useKeys, type ActionId } from "../keys"
|
|
18
|
-
import { useDialog } from "./dialog"
|
|
19
|
-
import { DialogSelect, type SelectOption } from "./dialog-select"
|
|
20
|
-
|
|
21
|
-
type Command = {
|
|
22
|
-
readonly title: string
|
|
23
|
-
readonly value: string
|
|
24
|
-
readonly action?: ActionId
|
|
25
|
-
readonly description?: string
|
|
26
|
-
readonly category?: string
|
|
27
|
-
readonly onSelect: () => void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type CommandContext = {
|
|
31
|
-
readonly register: (cmds: ReadonlyArray<Command>) => () => void
|
|
32
|
-
readonly setEnabled: (val: boolean) => void
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const Ctx = createContext<CommandContext | null>(null)
|
|
36
|
-
|
|
37
|
-
export const CommandProvider = ({ children }: { children: ReactNode }) => {
|
|
38
|
-
const registry = useRef<Map<string, ReadonlyArray<Command>>>(new Map())
|
|
39
|
-
const [, setRevision] = useState(0)
|
|
40
|
-
const enabled = useRef(true)
|
|
41
|
-
const dialog = useDialog()
|
|
42
|
-
const keys = useKeys()
|
|
43
|
-
|
|
44
|
-
const all = useCallback((): Command[] => {
|
|
45
|
-
const result: Command[] = []
|
|
46
|
-
registry.current.forEach(cmds => cmds.forEach(c => result.push(c)))
|
|
47
|
-
return result
|
|
48
|
-
}, [])
|
|
49
|
-
|
|
50
|
-
const register = useCallback((cmds: ReadonlyArray<Command>) => {
|
|
51
|
-
const id = String(Date.now()) + Math.random()
|
|
52
|
-
registry.current.set(id, cmds)
|
|
53
|
-
setRevision(r => r + 1)
|
|
54
|
-
return () => {
|
|
55
|
-
registry.current.delete(id)
|
|
56
|
-
setRevision(r => r + 1)
|
|
57
|
-
}
|
|
58
|
-
}, [])
|
|
59
|
-
|
|
60
|
-
const setEnabled = useCallback((val: boolean) => {
|
|
61
|
-
enabled.current = val
|
|
62
|
-
}, [])
|
|
63
|
-
|
|
64
|
-
const open = useCallback(() => {
|
|
65
|
-
const cmds = all()
|
|
66
|
-
const options: SelectOption[] = cmds.map(c => ({
|
|
67
|
-
title: c.title,
|
|
68
|
-
value: c.value,
|
|
69
|
-
description: c.description,
|
|
70
|
-
hint: c.action ? keys.print(c.action) : undefined,
|
|
71
|
-
category: c.category,
|
|
72
|
-
}))
|
|
73
|
-
dialog.replace(
|
|
74
|
-
<DialogSelect
|
|
75
|
-
title="Command Palette"
|
|
76
|
-
options={options}
|
|
77
|
-
onSelect={(opt) => {
|
|
78
|
-
dialog.clear()
|
|
79
|
-
const found = cmds.find(c => c.value === opt.value)
|
|
80
|
-
if (found) found.onSelect()
|
|
81
|
-
}}
|
|
82
|
-
placeholder="Search commands..."
|
|
83
|
-
/>
|
|
84
|
-
)
|
|
85
|
-
}, [all, dialog, keys])
|
|
86
|
-
|
|
87
|
-
useKeyboard((key) => {
|
|
88
|
-
if (!enabled.current || dialog.stack.length > 0) return
|
|
89
|
-
if (keys.match("palette.open", key)) return open()
|
|
90
|
-
for (const c of all()) {
|
|
91
|
-
if (c.action && keys.match(c.action, key)) return c.onSelect()
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
const value = useMemo<CommandContext>(() => ({ register, setEnabled }), [register, setEnabled])
|
|
96
|
-
|
|
97
|
-
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export const useCommand = (): CommandContext => {
|
|
101
|
-
const ctx = useContext(Ctx)
|
|
102
|
-
if (!ctx) throw new Error("useCommand() must be inside <CommandProvider>")
|
|
103
|
-
return ctx
|
|
104
|
-
}
|
package/src/ui/dialog-select.tsx
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Filterable select dialog — reusable pick-list for dialogs.
|
|
3
|
-
*
|
|
4
|
-
* Keyboard: up/down navigate, enter selects, typing filters.
|
|
5
|
-
* Mouse: hover highlights, click selects.
|
|
6
|
-
* Grouped by category with headers.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { useState, useMemo, useEffect, useRef } from "react"
|
|
10
|
-
import type { ReactNode } from "react"
|
|
11
|
-
import { useKeyboard } from "@opentui/react"
|
|
12
|
-
import type { ParsedKey, ScrollBoxRenderable } from "@opentui/core"
|
|
13
|
-
import { useTheme } from "../theme"
|
|
14
|
-
|
|
15
|
-
export type SelectOption = {
|
|
16
|
-
readonly title: string
|
|
17
|
-
readonly value: string
|
|
18
|
-
readonly description?: string
|
|
19
|
-
readonly hint?: string
|
|
20
|
-
readonly category?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type Props = {
|
|
24
|
-
readonly title: string
|
|
25
|
-
readonly options: ReadonlyArray<SelectOption>
|
|
26
|
-
readonly onSelect: (option: SelectOption) => void
|
|
27
|
-
readonly onMove?: (option: SelectOption) => void
|
|
28
|
-
/** Printable-key interceptor — return true to consume (skip filter append). */
|
|
29
|
-
readonly onKey?: (key: ParsedKey) => boolean
|
|
30
|
-
readonly placeholder?: string
|
|
31
|
-
readonly current?: string
|
|
32
|
-
readonly footer?: ReactNode
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const DialogSelect = (props: Props) => {
|
|
36
|
-
const [filter, setFilter] = useState("")
|
|
37
|
-
const [cursor, setCursor] = useState(0)
|
|
38
|
-
const sb = useRef<ScrollBoxRenderable | null>(null)
|
|
39
|
-
const theme = useTheme().theme
|
|
40
|
-
|
|
41
|
-
const filtered = useMemo(() => {
|
|
42
|
-
const lower = filter.toLowerCase()
|
|
43
|
-
return props.options.filter(o =>
|
|
44
|
-
o.title.toLowerCase().includes(lower) ||
|
|
45
|
-
(o.description ?? "").toLowerCase().includes(lower)
|
|
46
|
-
)
|
|
47
|
-
}, [filter, props.options])
|
|
48
|
-
|
|
49
|
-
// Group by category
|
|
50
|
-
const groups = useMemo(() => {
|
|
51
|
-
const map = new Map<string, SelectOption[]>()
|
|
52
|
-
filtered.forEach(o => {
|
|
53
|
-
const cat = o.category ?? ""
|
|
54
|
-
const arr = map.get(cat) ?? []
|
|
55
|
-
arr.push(o)
|
|
56
|
-
map.set(cat, arr)
|
|
57
|
-
})
|
|
58
|
-
return map
|
|
59
|
-
}, [filtered])
|
|
60
|
-
|
|
61
|
-
// Clamp cursor
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1))
|
|
64
|
-
}, [filtered.length, cursor])
|
|
65
|
-
|
|
66
|
-
const rowId = (i: number) => `ds-row-${i}`
|
|
67
|
-
|
|
68
|
-
const move = (n: number) => setCursor(c => {
|
|
69
|
-
const next = Math.max(0, Math.min(filtered.length - 1, c + n))
|
|
70
|
-
sb.current?.scrollChildIntoView(rowId(next))
|
|
71
|
-
return next
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// Notify on move
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
const item = filtered[cursor]
|
|
77
|
-
if (item && props.onMove) props.onMove(item)
|
|
78
|
-
}, [cursor, filtered, props.onMove])
|
|
79
|
-
|
|
80
|
-
useKeyboard((key) => {
|
|
81
|
-
if (key.name === "up") return move(-1)
|
|
82
|
-
if (key.name === "down") return move(1)
|
|
83
|
-
if (key.name === "pageup") return move(-10)
|
|
84
|
-
if (key.name === "pagedown") return move(10)
|
|
85
|
-
if (key.name === "return") {
|
|
86
|
-
const item = filtered[cursor]
|
|
87
|
-
if (item) props.onSelect(item)
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
if (props.onKey?.(key)) return
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// Build flat list with index tracking
|
|
94
|
-
let idx = 0
|
|
95
|
-
const entries = Array.from(groups.entries())
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<box flexDirection="column" width={60}>
|
|
99
|
-
<text fg={theme.text}>
|
|
100
|
-
<strong>{props.title}</strong>
|
|
101
|
-
</text>
|
|
102
|
-
<box height={1} />
|
|
103
|
-
<input
|
|
104
|
-
value={filter}
|
|
105
|
-
onInput={setFilter}
|
|
106
|
-
placeholder={props.placeholder ?? "Type to filter..."}
|
|
107
|
-
focused={true}
|
|
108
|
-
textColor={theme.text}
|
|
109
|
-
placeholderColor={theme.textMuted}
|
|
110
|
-
backgroundColor={theme.backgroundElement}
|
|
111
|
-
focusedBackgroundColor={theme.backgroundElement}
|
|
112
|
-
/>
|
|
113
|
-
<box height={1} />
|
|
114
|
-
{/* ScrollBox root is flex-row ([wrapper, v-scrollbar]); column stacking
|
|
115
|
-
belongs on the content box, not here. */}
|
|
116
|
-
<scrollbox ref={sb} scrollY maxHeight={16}
|
|
117
|
-
contentOptions={{ flexDirection: "column" }} paddingRight={1}>
|
|
118
|
-
{filtered.length === 0 ? (
|
|
119
|
-
<text fg={theme.textMuted}>{"No results found"}</text>
|
|
120
|
-
) : null}
|
|
121
|
-
{entries.map(([cat, items]) => {
|
|
122
|
-
const elements: React.ReactNode[] = []
|
|
123
|
-
if (cat) {
|
|
124
|
-
elements.push(
|
|
125
|
-
<text key={`cat-${cat}`} fg={theme.textMuted}>
|
|
126
|
-
<strong>{cat}</strong>
|
|
127
|
-
</text>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
items.forEach(item => {
|
|
131
|
-
const i = idx++
|
|
132
|
-
const active = i === cursor
|
|
133
|
-
const current = item.value === props.current
|
|
134
|
-
elements.push(
|
|
135
|
-
<box
|
|
136
|
-
key={item.value}
|
|
137
|
-
id={rowId(i)}
|
|
138
|
-
flexDirection="row"
|
|
139
|
-
backgroundColor={active ? theme.backgroundElement : undefined}
|
|
140
|
-
onMouseMove={() => setCursor(i)}
|
|
141
|
-
onMouseDown={() => props.onSelect(item)}
|
|
142
|
-
paddingLeft={1}
|
|
143
|
-
paddingRight={1}
|
|
144
|
-
>
|
|
145
|
-
<box flexGrow={1} height={1} overflow="hidden">
|
|
146
|
-
<text fg={active ? theme.text : theme.textMuted}>
|
|
147
|
-
{current ? "● " : " "}{item.title}{item.description ? ` — ${item.description}` : ""}
|
|
148
|
-
</text>
|
|
149
|
-
</box>
|
|
150
|
-
{item.hint ? (
|
|
151
|
-
<box flexShrink={0} height={1}>
|
|
152
|
-
<text fg={theme.textMuted}>{item.hint}</text>
|
|
153
|
-
</box>
|
|
154
|
-
) : null}
|
|
155
|
-
</box>
|
|
156
|
-
)
|
|
157
|
-
})
|
|
158
|
-
return elements
|
|
159
|
-
}).flat()}
|
|
160
|
-
</scrollbox>
|
|
161
|
-
{props.footer != null ? <box paddingTop={1}>{props.footer}</box> : null}
|
|
162
|
-
</box>
|
|
163
|
-
)
|
|
164
|
-
}
|
package/src/ui/dialog.tsx
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dialog overlay system — modal stack with backdrop.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* <DialogProvider><App /></DialogProvider>
|
|
6
|
-
*
|
|
7
|
-
* const dialog = useDialog()
|
|
8
|
-
* dialog.replace(<MyContent />, () => revert())
|
|
9
|
-
* dialog.clear()
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createContext, useContext, useState, useCallback, useMemo } from "react"
|
|
13
|
-
import type { ReactNode } from "react"
|
|
14
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react"
|
|
15
|
-
import { RGBA } from "@opentui/core"
|
|
16
|
-
import { useKeys } from "../keys"
|
|
17
|
-
import { useTheme } from "../theme"
|
|
18
|
-
|
|
19
|
-
type DialogEntry = {
|
|
20
|
-
readonly element: ReactNode
|
|
21
|
-
readonly onClose?: () => void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type DialogContext = {
|
|
25
|
-
readonly replace: (element: ReactNode, onClose?: () => void) => void
|
|
26
|
-
readonly clear: () => void
|
|
27
|
-
readonly stack: ReadonlyArray<DialogEntry>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const Ctx = createContext<DialogContext | null>(null)
|
|
31
|
-
|
|
32
|
-
const BACKDROP = RGBA.fromInts(0, 0, 0, 150)
|
|
33
|
-
|
|
34
|
-
export const DialogProvider = ({ children }: { children: ReactNode }) => {
|
|
35
|
-
const [stack, setStack] = useState<DialogEntry[]>([])
|
|
36
|
-
|
|
37
|
-
const replace = useCallback((element: ReactNode, onClose?: () => void) => {
|
|
38
|
-
setStack([{ element, onClose }])
|
|
39
|
-
}, [])
|
|
40
|
-
|
|
41
|
-
const clear = useCallback(() => {
|
|
42
|
-
setStack(prev => {
|
|
43
|
-
const top = prev[prev.length - 1]
|
|
44
|
-
if (top?.onClose) top.onClose()
|
|
45
|
-
return []
|
|
46
|
-
})
|
|
47
|
-
}, [])
|
|
48
|
-
|
|
49
|
-
const keys = useKeys()
|
|
50
|
-
useKeyboard((key) => {
|
|
51
|
-
if (stack.length === 0) return
|
|
52
|
-
if (keys.match("dialog.cancel", key)) clear()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const value = useMemo<DialogContext>(() => ({ replace, clear, stack }), [replace, clear, stack])
|
|
56
|
-
const top = stack.length > 0 ? stack[stack.length - 1] : undefined
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<Ctx.Provider value={value}>
|
|
60
|
-
{children}
|
|
61
|
-
{top ? <Overlay entry={top} onClose={clear} /> : null}
|
|
62
|
-
</Ctx.Provider>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const Overlay = ({ entry, onClose }: { entry: DialogEntry; onClose: () => void }) => {
|
|
67
|
-
const dims = useTerminalDimensions()
|
|
68
|
-
const theme = useTheme().theme
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<box
|
|
72
|
-
position="absolute"
|
|
73
|
-
left={0}
|
|
74
|
-
top={0}
|
|
75
|
-
width={dims.width}
|
|
76
|
-
height={dims.height}
|
|
77
|
-
zIndex={100}
|
|
78
|
-
backgroundColor={BACKDROP}
|
|
79
|
-
justifyContent="center"
|
|
80
|
-
alignItems="center"
|
|
81
|
-
onMouseDown={onClose}
|
|
82
|
-
>
|
|
83
|
-
<box
|
|
84
|
-
backgroundColor={theme.backgroundPanel}
|
|
85
|
-
borderStyle="single"
|
|
86
|
-
border={true}
|
|
87
|
-
borderColor={theme.border}
|
|
88
|
-
padding={1}
|
|
89
|
-
flexDirection="column"
|
|
90
|
-
onMouseDown={(e) => { e.stopPropagation() }}
|
|
91
|
-
>
|
|
92
|
-
{entry.element}
|
|
93
|
-
</box>
|
|
94
|
-
</box>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export const useDialog = (): DialogContext => {
|
|
99
|
-
const ctx = useContext(Ctx)
|
|
100
|
-
if (!ctx) throw new Error("useDialog() must be inside <DialogProvider>")
|
|
101
|
-
return ctx
|
|
102
|
-
}
|
package/src/ui/fmt.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Shared string formatters for tab panels.
|
|
2
|
-
//
|
|
3
|
-
// Timestamps in this codebase are unix seconds unless otherwise noted
|
|
4
|
-
// (gateway RPCs and state.db both store seconds). `ago`/`when`/`span`
|
|
5
|
-
// take seconds; `dur` takes an already-computed delta in seconds.
|
|
6
|
-
//
|
|
7
|
-
// `timeFormat` / `timeStyle` preferences are read via prefs.get() —
|
|
8
|
-
// non-reactive, same as `animations`. tui.json is loaded once at
|
|
9
|
-
// launch; a live toggle (herm-907.6) will re-render via usePref() at
|
|
10
|
-
// the toggle site, and these stay plain functions.
|
|
11
|
-
|
|
12
|
-
import * as prefs from "../utils/preferences"
|
|
13
|
-
|
|
14
|
-
const h12 = () => prefs.get("timeFormat") === "12h"
|
|
15
|
-
const abs = () => prefs.get("timeStyle") === "absolute"
|
|
16
|
-
|
|
17
|
-
export const trunc = (s: string, max: number): string =>
|
|
18
|
-
s.length <= max ? s : s.slice(0, max - 1) + "…"
|
|
19
|
-
|
|
20
|
-
// abbreviate large counts: 12.3k / 1.23M
|
|
21
|
-
export const fmt = (n: number): string =>
|
|
22
|
-
n >= 1_000_000 ? `${(n / 1_000_000).toFixed(2)}M`
|
|
23
|
-
: n >= 1_000 ? `${(n / 1_000).toFixed(1)}k`
|
|
24
|
-
: String(n)
|
|
25
|
-
|
|
26
|
-
export const cost = (c: number | null | undefined): string =>
|
|
27
|
-
c == null ? "—" : `$${c.toFixed(2)}`
|
|
28
|
-
|
|
29
|
-
const clock = (d: Date) =>
|
|
30
|
-
d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: h12() })
|
|
31
|
-
|
|
32
|
-
// Compact absolute stamp for list columns (≤8 chars at 24h, ≤7 at 12h):
|
|
33
|
-
// today → HH:MM, otherwise → "May 1". Hoisted from Sessions.
|
|
34
|
-
export const stamp = (ts: number): string => {
|
|
35
|
-
const d = new Date(ts * 1000)
|
|
36
|
-
return d.toDateString() === new Date().toDateString()
|
|
37
|
-
? clock(d)
|
|
38
|
-
: d.toLocaleDateString(undefined, { month: "short", day: "numeric" })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const rel = (ts: number): string => {
|
|
42
|
-
const s = Math.floor(Date.now() / 1000 - ts)
|
|
43
|
-
if (s < 60) return "just now"
|
|
44
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`
|
|
45
|
-
if (s < 86400) return `${Math.floor(s / 3600)}h ago`
|
|
46
|
-
return `${Math.floor(s / 86400)}d ago`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// `timeStyle` flips ago/until between relative deltas and compact
|
|
50
|
-
// absolute stamps. Callers are list columns and inline "· 2h ago"
|
|
51
|
-
// labels — both read fine as "· 14:32" / "· May 1".
|
|
52
|
-
export const ago = (ts: number): string => abs() ? stamp(ts) : rel(ts)
|
|
53
|
-
|
|
54
|
-
// Future counterpart to `ago`. Past → "due".
|
|
55
|
-
export const until = (ts: number): string => {
|
|
56
|
-
if (abs()) return ts <= Date.now() / 1000 ? "due" : stamp(ts)
|
|
57
|
-
const s = Math.floor(ts - Date.now() / 1000)
|
|
58
|
-
if (s <= 0) return "due"
|
|
59
|
-
if (s < 60) return `in ${s}s`
|
|
60
|
-
if (s < 3600) return `in ${Math.floor(s / 60)}m`
|
|
61
|
-
if (s < 86400) return `in ${Math.floor(s / 3600)}h`
|
|
62
|
-
return `in ${Math.floor(s / 86400)}d`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const when = (ts: number): string => {
|
|
66
|
-
const d = new Date(ts * 1000)
|
|
67
|
-
return `${d.toLocaleDateString()} ${clock(d)}`
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const span = (start: number, end: number): string => {
|
|
71
|
-
const s = Math.round(end - start)
|
|
72
|
-
if (s < 0) return "—"
|
|
73
|
-
if (s >= 3600) return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`
|
|
74
|
-
if (s >= 60) return `${Math.floor(s / 60)}m`
|
|
75
|
-
return `${s}s`
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// compact duration for uptime columns (no spaces, always two units ≥1m)
|
|
79
|
-
export const dur = (s: number): string =>
|
|
80
|
-
s >= 3600 ? `${Math.floor(s / 3600)}h${Math.floor((s % 3600) / 60)}m`
|
|
81
|
-
: s >= 60 ? `${Math.floor(s / 60)}m${Math.floor(s % 60)}s`
|
|
82
|
-
: `${Math.floor(s)}s`
|
package/src/ui/kv.tsx
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react"
|
|
2
|
-
import type { RGBA } from "@opentui/core"
|
|
3
|
-
import { useTheme } from "../theme"
|
|
4
|
-
|
|
5
|
-
// Single key/value line for detail panels. Label is a fixed-width
|
|
6
|
-
// muted column; value takes remaining width. Default hard-truncates
|
|
7
|
-
// at one line (overflow=hidden on height=1) so a long value can't
|
|
8
|
-
// push panel layout; `wrap` opts into multi-line word-wrap instead.
|
|
9
|
-
export const KV = (props: { label: string; value: string; fg?: RGBA; wrap?: boolean }) => {
|
|
10
|
-
const theme = useTheme().theme
|
|
11
|
-
return (
|
|
12
|
-
<box minHeight={1} flexDirection="row">
|
|
13
|
-
<box width={13} flexShrink={0}><text fg={theme.textMuted}>{props.label}</text></box>
|
|
14
|
-
<box flexGrow={1} minWidth={0}
|
|
15
|
-
height={props.wrap ? undefined : 1}
|
|
16
|
-
overflow={props.wrap ? undefined : "hidden"}>
|
|
17
|
-
<text wrapMode={props.wrap ? "word" : undefined} fg={props.fg ?? theme.text}>{props.value}</text>
|
|
18
|
-
</box>
|
|
19
|
-
</box>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Stack of KV lines. Rows with an undefined value are skipped so
|
|
24
|
-
// callers can inline conditionals without ternary noise.
|
|
25
|
-
export const KVBlock = (props: { rows: Array<[string, string | undefined, RGBA?]> }): ReactNode =>
|
|
26
|
-
props.rows.map(([k, v, fg]) =>
|
|
27
|
-
v === undefined ? null : <KV key={k} label={k} value={v} fg={fg} />,
|
|
28
|
-
)
|
package/src/ui/shell.tsx
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react"
|
|
2
|
-
import { useTheme } from "../theme"
|
|
3
|
-
|
|
4
|
-
// Bordered panel chrome shared by every tab: title + keybinding hint
|
|
5
|
-
// on one header line, optional error line, a one-row gap, then the
|
|
6
|
-
// body. Body is wrapped in a flexGrow column with minWidth=0 so
|
|
7
|
-
// children can truncate instead of forcing the panel wider than the
|
|
8
|
-
// terminal.
|
|
9
|
-
//
|
|
10
|
-
// `focus` switches the border to theme.primary — used when a tab
|
|
11
|
-
// hosts multiple panels and wants to show which has keyboard focus.
|
|
12
|
-
// `grow` lets side-by-side panels set their flex ratio directly;
|
|
13
|
-
// flexBasis=0 makes the ratio authoritative regardless of content.
|
|
14
|
-
|
|
15
|
-
export const TabShell = (props: {
|
|
16
|
-
title: string
|
|
17
|
-
hint: string
|
|
18
|
-
error?: string | null
|
|
19
|
-
focus?: boolean
|
|
20
|
-
grow?: number
|
|
21
|
-
children?: ReactNode
|
|
22
|
-
}) => {
|
|
23
|
-
const theme = useTheme().theme
|
|
24
|
-
return (
|
|
25
|
-
<box flexDirection="column" flexGrow={props.grow ?? 1} flexBasis={0} minWidth={0}
|
|
26
|
-
border borderColor={props.focus ? theme.primary : theme.border}
|
|
27
|
-
backgroundColor={theme.backgroundPanel} padding={1}>
|
|
28
|
-
<box height={1} flexDirection="row" overflow="hidden">
|
|
29
|
-
<box flexShrink={0}>
|
|
30
|
-
<text fg={theme.primary}><strong>{props.title}</strong></text>
|
|
31
|
-
</box>
|
|
32
|
-
<box flexGrow={1} minWidth={0} height={1} overflow="hidden">
|
|
33
|
-
<text fg={theme.textMuted}>{` ${props.hint}`}</text>
|
|
34
|
-
</box>
|
|
35
|
-
</box>
|
|
36
|
-
{props.error
|
|
37
|
-
? <box height={1}><text fg={theme.error}>{`⚠ ${props.error}`}</text></box>
|
|
38
|
-
: null}
|
|
39
|
-
<box height={1} />
|
|
40
|
-
<box flexDirection="column" flexGrow={1} minWidth={0}>
|
|
41
|
-
{props.children}
|
|
42
|
-
</box>
|
|
43
|
-
</box>
|
|
44
|
-
)
|
|
45
|
-
}
|
package/src/ui/spinner.tsx
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// Single shared braille spinner. oc uses a native <spinner> element
|
|
2
|
-
// from opentui-spinner; Herm drives a setInterval so the only moving
|
|
3
|
-
// state is one integer. All spinners on screen share the same tick
|
|
4
|
-
// via a module-level clock — N spinners = 1 interval, not N.
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect, memo, type ReactNode } from "react"
|
|
7
|
-
import type { RGBA } from "@opentui/core"
|
|
8
|
-
import { useTheme } from "../theme"
|
|
9
|
-
import * as prefs from "../utils/preferences"
|
|
10
|
-
|
|
11
|
-
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
12
|
-
const MS = 80
|
|
13
|
-
|
|
14
|
-
type Sub = (n: number) => void
|
|
15
|
-
const subs = new Set<Sub>()
|
|
16
|
-
let tick = 0
|
|
17
|
-
let timer: ReturnType<typeof setInterval> | null = null
|
|
18
|
-
|
|
19
|
-
function sub(fn: Sub) {
|
|
20
|
-
subs.add(fn)
|
|
21
|
-
if (!timer) timer = setInterval(() => {
|
|
22
|
-
tick = (tick + 1) % FRAMES.length
|
|
23
|
-
for (const s of subs) s(tick)
|
|
24
|
-
}, MS)
|
|
25
|
-
return () => {
|
|
26
|
-
subs.delete(fn)
|
|
27
|
-
if (subs.size === 0 && timer) { clearInterval(timer); timer = null }
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function useFrame(active: boolean): number {
|
|
32
|
-
const [n, set] = useState(tick)
|
|
33
|
-
useEffect(() => (active ? sub(set) : undefined), [active])
|
|
34
|
-
return n
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const Spinner = memo((props: { color?: RGBA; label?: ReactNode }) => {
|
|
38
|
-
const theme = useTheme().theme
|
|
39
|
-
const color = props.color ?? theme.textMuted
|
|
40
|
-
const on = prefs.get("animations") !== false
|
|
41
|
-
const n = useFrame(on)
|
|
42
|
-
return (
|
|
43
|
-
<text>
|
|
44
|
-
<span fg={color}>{on ? FRAMES[n] : "⋯"}</span>
|
|
45
|
-
{props.label ? <span fg={color}> {props.label}</span> : null}
|
|
46
|
-
</text>
|
|
47
|
-
)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Inline glyph only — for embedding inside an existing <text>. Pass
|
|
52
|
-
* `active=false` for rows that aren't spinning: the hook won't
|
|
53
|
-
* subscribe to the clock, so completed rows don't re-render 12×/s.
|
|
54
|
-
*/
|
|
55
|
-
export function useSpinnerGlyph(active = true): string {
|
|
56
|
-
const on = prefs.get("animations") !== false && active
|
|
57
|
-
const n = useFrame(on)
|
|
58
|
-
return on ? FRAMES[n] : "⋯"
|
|
59
|
-
}
|