herm-tui 1.0.0-dev.1 → 1.0.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db.worker.js +81 -0
- package/highlights-eq9cgrbb.scm +604 -0
- package/highlights-ghv9g403.scm +205 -0
- package/highlights-hk7bwhj4.scm +284 -0
- package/highlights-r812a2qc.scm +150 -0
- package/highlights-x6tmsnaa.scm +115 -0
- package/index.js +10374 -0
- package/injections-73j83es3.scm +27 -0
- package/package.json +14 -64
- package/parser.worker.js +8 -0
- package/tree-sitter-3jzf13jk.wasm +0 -0
- package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/scripts/postinstall.ts +0 -29
- package/src/app/gateway.tsx +0 -83
- package/src/app/gatewayEvents.ts +0 -203
- package/src/app/launch.ts +0 -41
- package/src/app/skin.tsx +0 -31
- package/src/app/spawnHistory.ts +0 -75
- package/src/app/tabs.ts +0 -23
- package/src/app/turnReducer.ts +0 -390
- package/src/app/useAppKeys.ts +0 -268
- package/src/app/useAtRefPopover.ts +0 -99
- package/src/app/useInputHistory.ts +0 -66
- package/src/app/useSession.ts +0 -102
- package/src/app/useSlashCommands.ts +0 -70
- package/src/app/useSlashPopover.ts +0 -48
- package/src/app.tsx +0 -917
- package/src/commands/slash.ts +0 -151
- package/src/components/avatar/AnimatedAvatar.tsx +0 -66
- package/src/components/avatar/eikon.ts +0 -144
- package/src/components/avatar/states/error.ts +0 -1155
- package/src/components/avatar/states/idle.ts +0 -1155
- package/src/components/avatar/states/index.ts +0 -30
- package/src/components/avatar/states/listening.ts +0 -1155
- package/src/components/avatar/states/speaking.ts +0 -1155
- package/src/components/avatar/states/thinking.ts +0 -1155
- package/src/components/avatar/states/working.ts +0 -1155
- package/src/components/chat/AtRefPopover.tsx +0 -54
- package/src/components/chat/CodeBlock.tsx +0 -67
- package/src/components/chat/Composer.tsx +0 -347
- package/src/components/chat/DiffBlock.tsx +0 -116
- package/src/components/chat/ErrorBlock.tsx +0 -70
- package/src/components/chat/MediaChip.tsx +0 -114
- package/src/components/chat/MessageItem.tsx +0 -282
- package/src/components/chat/MessageList.tsx +0 -114
- package/src/components/chat/PromptCard.tsx +0 -359
- package/src/components/chat/SlashPopover.tsx +0 -158
- package/src/components/chat/ThoughtCloud.tsx +0 -185
- package/src/components/chat/TypingIndicator.tsx +0 -25
- package/src/components/chat/tool/Subagent.tsx +0 -75
- package/src/components/chat/tool/frame.tsx +0 -69
- package/src/components/chat/tool/index.tsx +0 -65
- package/src/components/chat/tool/preview.ts +0 -57
- package/src/components/sidebar/ContextGauge.tsx +0 -102
- package/src/components/sidebar/Sidebar.tsx +0 -143
- package/src/components/tabs/TabBar.tsx +0 -50
- package/src/components/ui/FileLink.tsx +0 -52
- package/src/config/index.ts +0 -156
- package/src/config/lane.ts +0 -161
- package/src/config/models.ts +0 -95
- package/src/config/rules.ts +0 -80
- package/src/config/schema.ts +0 -308
- package/src/dialogs/alert.tsx +0 -52
- package/src/dialogs/chafa.tsx +0 -72
- package/src/dialogs/confirm.tsx +0 -58
- package/src/dialogs/curator.tsx +0 -153
- package/src/dialogs/eikon-picker.tsx +0 -95
- package/src/dialogs/help.tsx +0 -80
- package/src/dialogs/history.tsx +0 -92
- package/src/dialogs/info.tsx +0 -115
- package/src/dialogs/keys.tsx +0 -170
- package/src/dialogs/logs.tsx +0 -42
- package/src/dialogs/message.tsx +0 -38
- package/src/dialogs/model-picker.tsx +0 -123
- package/src/dialogs/new-profile.tsx +0 -69
- package/src/dialogs/new-task.tsx +0 -103
- package/src/dialogs/profile.tsx +0 -55
- package/src/dialogs/rollback.tsx +0 -190
- package/src/dialogs/spawn-history.tsx +0 -80
- package/src/dialogs/text-prompt.tsx +0 -68
- package/src/dialogs/theme-picker.tsx +0 -50
- package/src/home/index.ts +0 -23
- package/src/home/store.ts +0 -267
- package/src/index.tsx +0 -113
- package/src/keys/catalog.ts +0 -115
- package/src/keys/chord.ts +0 -125
- package/src/keys/conflicts.ts +0 -48
- package/src/keys/context.tsx +0 -112
- package/src/keys/index.ts +0 -5
- package/src/keys/list.ts +0 -94
- package/src/keys/oc-compat.ts +0 -87
- package/src/tabs/Agents.tsx +0 -607
- package/src/tabs/Analytics.tsx +0 -154
- package/src/tabs/Chat.tsx +0 -50
- package/src/tabs/Config.tsx +0 -605
- package/src/tabs/Context.tsx +0 -599
- package/src/tabs/Cron.tsx +0 -294
- package/src/tabs/Env.tsx +0 -227
- package/src/tabs/Kanban.tsx +0 -367
- package/src/tabs/Memory.tsx +0 -294
- package/src/tabs/Sessions.tsx +0 -786
- package/src/tabs/Skills.tsx +0 -507
- package/src/tabs/Toolsets.tsx +0 -266
- package/src/theme/builtin.ts +0 -78
- package/src/theme/context.tsx +0 -106
- package/src/theme/index.ts +0 -4
- package/src/theme/resolve.ts +0 -134
- package/src/theme/syntax.ts +0 -31
- package/src/theme/themes/aura.json +0 -69
- package/src/theme/themes/ayu.json +0 -80
- package/src/theme/themes/carbonfox.json +0 -248
- package/src/theme/themes/catppuccin-frappe.json +0 -233
- package/src/theme/themes/catppuccin-macchiato.json +0 -233
- package/src/theme/themes/catppuccin.json +0 -112
- package/src/theme/themes/cobalt2.json +0 -228
- package/src/theme/themes/cursor.json +0 -249
- package/src/theme/themes/dracula.json +0 -219
- package/src/theme/themes/everforest.json +0 -241
- package/src/theme/themes/flexoki.json +0 -237
- package/src/theme/themes/github.json +0 -233
- package/src/theme/themes/gruvbox.json +0 -242
- package/src/theme/themes/kanagawa.json +0 -77
- package/src/theme/themes/lucent-orng.json +0 -237
- package/src/theme/themes/material.json +0 -235
- package/src/theme/themes/matrix.json +0 -77
- package/src/theme/themes/mercury.json +0 -252
- package/src/theme/themes/monokai.json +0 -221
- package/src/theme/themes/nightowl.json +0 -221
- package/src/theme/themes/nord.json +0 -223
- package/src/theme/themes/one-dark.json +0 -84
- package/src/theme/themes/opencode.json +0 -245
- package/src/theme/themes/orng.json +0 -249
- package/src/theme/themes/osaka-jade.json +0 -93
- package/src/theme/themes/palenight.json +0 -222
- package/src/theme/themes/rosepine.json +0 -234
- package/src/theme/themes/solarized.json +0 -223
- package/src/theme/themes/synthwave84.json +0 -226
- package/src/theme/themes/tokyonight.json +0 -243
- package/src/theme/themes/vercel.json +0 -245
- package/src/theme/themes/vesper.json +0 -218
- package/src/theme/themes/zenburn.json +0 -223
- package/src/theme/types.ts +0 -119
- package/src/types/message.ts +0 -97
- package/src/ui/ChafaImage.tsx +0 -64
- package/src/ui/Splash.tsx +0 -118
- package/src/ui/borders.ts +0 -28
- package/src/ui/command.tsx +0 -104
- package/src/ui/dialog-select.tsx +0 -164
- package/src/ui/dialog.tsx +0 -102
- package/src/ui/fmt.ts +0 -82
- package/src/ui/kv.tsx +0 -28
- package/src/ui/shell.tsx +0 -45
- package/src/ui/spinner.tsx +0 -59
- package/src/ui/splash-art.ts +0 -123
- package/src/ui/table.tsx +0 -117
- package/src/ui/ticker.tsx +0 -90
- package/src/ui/toast.tsx +0 -130
- package/src/utils/categorical.ts +0 -77
- package/src/utils/chafa.ts +0 -173
- package/src/utils/clipboard.ts +0 -67
- package/src/utils/context-segments.ts +0 -317
- package/src/utils/control.ts +0 -495
- package/src/utils/drop.ts +0 -25
- package/src/utils/editor.ts +0 -33
- package/src/utils/fuzzy.ts +0 -45
- package/src/utils/gateway-client.ts +0 -253
- package/src/utils/gateway-types.ts +0 -282
- package/src/utils/git.ts +0 -57
- package/src/utils/hermes-analytics.ts +0 -134
- package/src/utils/hermes-home.ts +0 -821
- package/src/utils/hermes-kanban.ts +0 -154
- package/src/utils/hermes-profiles.ts +0 -217
- package/src/utils/interpolate.ts +0 -31
- package/src/utils/math-unicode.ts +0 -818
- package/src/utils/memory-activity.ts +0 -140
- package/src/utils/open-file.ts +0 -13
- package/src/utils/paths.ts +0 -52
- package/src/utils/perf.ts +0 -235
- package/src/utils/preferences.ts +0 -150
- package/src/utils/sessions-db.ts +0 -396
- package/src/utils/subagent-tree.ts +0 -146
- package/src/utils/terminal-reset.ts +0 -129
- package/src/utils/tips.ts +0 -67
- package/src/utils/tokens.ts +0 -87
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// MEDIA: directive rendering — `MEDIA:/path` lines in assistant output
|
|
2
|
-
// become clickable chips instead of literal text. Neither reference
|
|
3
|
-
// (Ink nor opencode) renders pixels; both surface an openable link.
|
|
4
|
-
// OpenTUI has no image primitive, so this is the ceiling for now.
|
|
5
|
-
|
|
6
|
-
import { memo, useState } from "react"
|
|
7
|
-
import { TextAttributes, type MouseEvent } from "@opentui/core"
|
|
8
|
-
import { openFile } from "../../utils/open-file"
|
|
9
|
-
import { useTheme } from "../../theme"
|
|
10
|
-
|
|
11
|
-
// Ink's canonical regex. Match-per-line only — a MEDIA path is the
|
|
12
|
-
// whole line, optionally wrapped in backticks/quotes by the model.
|
|
13
|
-
export const MEDIA_LINE_RE = /^\s*[`"']?MEDIA:\s*(\S+?)[`"']?\s*$/
|
|
14
|
-
|
|
15
|
-
const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"])
|
|
16
|
-
const AUDIO_EXT = new Set(["mp3", "wav", "ogg", "m4a", "flac", "opus"])
|
|
17
|
-
const VIDEO_EXT = new Set(["mp4", "webm", "mov", "mkv"])
|
|
18
|
-
|
|
19
|
-
export type MediaKind = "img" | "audio" | "video" | "file" | "url"
|
|
20
|
-
|
|
21
|
-
export function classify(path: string): MediaKind {
|
|
22
|
-
if (/^https?:\/\//i.test(path)) return "url"
|
|
23
|
-
const ext = path.split(".").pop()?.toLowerCase() ?? ""
|
|
24
|
-
if (IMAGE_EXT.has(ext)) return "img"
|
|
25
|
-
if (AUDIO_EXT.has(ext)) return "audio"
|
|
26
|
-
if (VIDEO_EXT.has(ext)) return "video"
|
|
27
|
-
return "file"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const basename = (p: string) => p.split(/[/\\]/).pop() || p
|
|
31
|
-
|
|
32
|
-
export type Seg = { md: string } | { media: string } | { code: string; lang?: string }
|
|
33
|
-
|
|
34
|
-
// Split text into alternating markdown / media / code segments.
|
|
35
|
-
// Adjacent markdown lines are re-joined so OpenTUI's MarkdownRenderable
|
|
36
|
-
// still sees complete paragraphs. MEDIA lines inside fenced code are
|
|
37
|
-
// left as literal text (they're examples, not directives). Fences emit
|
|
38
|
-
// a `code` segment so MessageItem can wrap them with chrome; a trailing
|
|
39
|
-
// unclosed fence stays in the markdown buffer so streaming output
|
|
40
|
-
// doesn't flash into a CodeBlock mid-word.
|
|
41
|
-
export function splitContent(text: string): Seg[] {
|
|
42
|
-
if (!text.includes("MEDIA:") && !text.includes("```") && !text.includes("~~~"))
|
|
43
|
-
return [{ md: text }]
|
|
44
|
-
const out: Seg[] = []
|
|
45
|
-
let buf: string[] = []
|
|
46
|
-
let fence: { mark: string; lang?: string; body: string[] } | null = null
|
|
47
|
-
const flush = () => {
|
|
48
|
-
if (buf.length) out.push({ md: buf.join("\n") })
|
|
49
|
-
buf = []
|
|
50
|
-
}
|
|
51
|
-
for (const line of text.split("\n")) {
|
|
52
|
-
const f = line.match(/^\s*(`{3,}|~{3,})\s*(\S*)/)
|
|
53
|
-
if (f) {
|
|
54
|
-
if (fence && f[1][0] === fence.mark[0] && f[1].length >= fence.mark.length) {
|
|
55
|
-
out.push({ code: fence.body.join("\n"), lang: fence.lang || undefined })
|
|
56
|
-
fence = null
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
if (!fence) {
|
|
60
|
-
flush()
|
|
61
|
-
fence = { mark: f[1], lang: f[2], body: [] }
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (fence) { fence.body.push(line); continue }
|
|
66
|
-
const m = line.match(MEDIA_LINE_RE)?.[1]
|
|
67
|
-
if (m) { flush(); out.push({ media: m }); continue }
|
|
68
|
-
buf.push(line)
|
|
69
|
-
}
|
|
70
|
-
// Unclosed fence → put it back verbatim so the markdown renderable
|
|
71
|
-
// shows the partial block while the stream is still producing it.
|
|
72
|
-
if (fence) {
|
|
73
|
-
const tail = [fence.mark + (fence.lang ?? ""), ...fence.body].join("\n")
|
|
74
|
-
const last = out[out.length - 1]
|
|
75
|
-
if (last && "md" in last) last.md += "\n" + tail
|
|
76
|
-
else buf.push(tail)
|
|
77
|
-
}
|
|
78
|
-
flush()
|
|
79
|
-
return out
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const MediaChip = memo((props: {
|
|
83
|
-
path: string
|
|
84
|
-
/** Override the default open-file click. Handlers stopPropagation so
|
|
85
|
-
* the enclosing message's useClick (→ actions menu) never sees the
|
|
86
|
-
* down event. Pass to repurpose the chip (e.g. ChafaImage collapse). */
|
|
87
|
-
onMouseDown?: (e: MouseEvent) => void
|
|
88
|
-
}) => {
|
|
89
|
-
const theme = useTheme().theme
|
|
90
|
-
const [hover, setHover] = useState(false)
|
|
91
|
-
const kind = classify(props.path)
|
|
92
|
-
const badge = {
|
|
93
|
-
img: theme.accent, audio: theme.warning, video: theme.info,
|
|
94
|
-
url: theme.primary, file: theme.secondary,
|
|
95
|
-
}[kind]
|
|
96
|
-
const click = props.onMouseDown
|
|
97
|
-
?? ((e: MouseEvent) => { e.stopPropagation(); openFile(props.path) })
|
|
98
|
-
return (
|
|
99
|
-
<box
|
|
100
|
-
flexDirection="row" height={1}
|
|
101
|
-
onMouseDown={click}
|
|
102
|
-
onMouseOver={() => setHover(true)}
|
|
103
|
-
onMouseOut={() => setHover(false)}
|
|
104
|
-
>
|
|
105
|
-
<text>
|
|
106
|
-
<span bg={badge} fg={theme.background}> {kind} </span>
|
|
107
|
-
<span bg={theme.backgroundElement} fg={theme.text}
|
|
108
|
-
attributes={hover ? TextAttributes.UNDERLINE : TextAttributes.NONE}>
|
|
109
|
-
{" "}{basename(props.path)}{" "}
|
|
110
|
-
</span>
|
|
111
|
-
</text>
|
|
112
|
-
</box>
|
|
113
|
-
)
|
|
114
|
-
})
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { memo, useMemo, useRef, useState, type RefObject } from "react"
|
|
2
|
-
import type { RGBA, MouseEvent } from "@opentui/core"
|
|
3
|
-
import type { Message, Part, TextPart, ToolPart, PromptPart } from "../../types/message"
|
|
4
|
-
import { ErrorBlock } from "./ErrorBlock"
|
|
5
|
-
import { MediaChip, classify, splitContent } from "./MediaChip"
|
|
6
|
-
import { CodeBlock } from "./CodeBlock"
|
|
7
|
-
import { DiffBlock, isDiff } from "./DiffBlock"
|
|
8
|
-
import { PromptCard, type PromptCardHandle } from "./PromptCard"
|
|
9
|
-
import { ChafaImage } from "../../ui/ChafaImage"
|
|
10
|
-
import { useTheme } from "../../theme"
|
|
11
|
-
import { useSkin } from "../../app/skin"
|
|
12
|
-
import { mathify } from "../../utils/math-unicode"
|
|
13
|
-
|
|
14
|
-
export type { Message }
|
|
15
|
-
|
|
16
|
-
function duration(ms: number): string {
|
|
17
|
-
if (ms < 1000) return `${ms}ms`
|
|
18
|
-
const s = ms / 1000
|
|
19
|
-
if (s < 60) return `${s.toFixed(1)}s`
|
|
20
|
-
return `${Math.floor(s / 60)}m${Math.floor(s % 60)}s`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function tokens(n: number): string {
|
|
24
|
-
if (n < 1000) return String(n)
|
|
25
|
-
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`
|
|
26
|
-
return `${(n / 1_000_000).toFixed(2)}M`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function extract(msg: Message): string {
|
|
30
|
-
return msg.parts
|
|
31
|
-
.filter((p): p is TextPart => p.type === "text")
|
|
32
|
-
.map(p => p.content)
|
|
33
|
-
.join("")
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const trunc = (s: string, max: number) => s.length <= max ? s : s.slice(0, max - 1) + "…"
|
|
37
|
-
|
|
38
|
-
// Collapsible diff chip: shows filename/preview + +N/-M, expands to full
|
|
39
|
-
// DiffBlock on click. Lives in the message body so edits land in the
|
|
40
|
-
// transcript (not buried in the ThoughtCloud). stopPropagation keeps the
|
|
41
|
-
// click from triggering onPick on the parent message.
|
|
42
|
-
const InlineDiff = memo(({ tool }: { tool: ToolPart }) => {
|
|
43
|
-
const theme = useTheme().theme
|
|
44
|
-
const [open, setOpen] = useState(false)
|
|
45
|
-
const diff = tool.diff ?? (isDiff(tool.result) ? tool.result : undefined)
|
|
46
|
-
if (!diff) return null
|
|
47
|
-
const lines = diff.split("\n")
|
|
48
|
-
const add = lines.filter(l => /^\+(?!\+\+)/.test(l)).length
|
|
49
|
-
const del = lines.filter(l => /^-(?!--)/.test(l)).length
|
|
50
|
-
return (
|
|
51
|
-
<box flexDirection="column" marginTop={1}
|
|
52
|
-
onMouseDown={(e: MouseEvent) => { e.stopPropagation(); setOpen(o => !o) }}>
|
|
53
|
-
<box height={1}>
|
|
54
|
-
<text>
|
|
55
|
-
<span fg={theme.textMuted}>{open ? "▾ " : "▸ "}</span>
|
|
56
|
-
<span fg={theme.text}>{trunc(tool.preview ?? tool.name, 50)}</span>
|
|
57
|
-
<span fg={theme.textMuted}> </span>
|
|
58
|
-
<span fg={theme.success}>+{add}</span>
|
|
59
|
-
<span fg={theme.textMuted}> / </span>
|
|
60
|
-
<span fg={theme.error}>-{del}</span>
|
|
61
|
-
</text>
|
|
62
|
-
</box>
|
|
63
|
-
{open ? <box marginTop={1}><DiffBlock text={diff} /></box> : null}
|
|
64
|
-
</box>
|
|
65
|
-
)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// OpenTUI has no onClick; synthesize one from down→up at the same cell
|
|
69
|
-
// so text-selection drags don't fire it.
|
|
70
|
-
function useClick(fn?: () => void) {
|
|
71
|
-
const at = useRef<{ x: number; y: number } | null>(null)
|
|
72
|
-
return {
|
|
73
|
-
onMouseDown: (e: MouseEvent) => { at.current = { x: e.x, y: e.y } },
|
|
74
|
-
onMouseUp: (e: MouseEvent) => {
|
|
75
|
-
const a = at.current
|
|
76
|
-
at.current = null
|
|
77
|
-
if (fn && a && a.x === e.x && a.y === e.y) fn()
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Themed vertical bar next to the body. `side` picks left or right. */
|
|
83
|
-
const Gutter = memo(({ color, glyph = "│", side = "left", children }: {
|
|
84
|
-
color: RGBA
|
|
85
|
-
glyph?: string
|
|
86
|
-
side?: "left" | "right"
|
|
87
|
-
children: React.ReactNode
|
|
88
|
-
}) => {
|
|
89
|
-
const bar = (
|
|
90
|
-
<box
|
|
91
|
-
width={2}
|
|
92
|
-
flexShrink={0}
|
|
93
|
-
border={[side]}
|
|
94
|
-
borderColor={color}
|
|
95
|
-
customBorderChars={{
|
|
96
|
-
topLeft: glyph, bottomLeft: glyph, vertical: glyph,
|
|
97
|
-
topRight: glyph, bottomRight: glyph, horizontal: "",
|
|
98
|
-
topT: "", bottomT: "", leftT: "", rightT: "", cross: "",
|
|
99
|
-
}}
|
|
100
|
-
/>
|
|
101
|
-
)
|
|
102
|
-
return (
|
|
103
|
-
<box flexDirection="row">
|
|
104
|
-
{side === "left" ? bar : null}
|
|
105
|
-
<box flexDirection="column" flexGrow={1} flexShrink={1}>
|
|
106
|
-
{children}
|
|
107
|
-
</box>
|
|
108
|
-
{side === "right" ? bar : null}
|
|
109
|
-
</box>
|
|
110
|
-
)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
export type PromptWire = {
|
|
114
|
-
/** Ref to the single pending prompt card, for key routing. */
|
|
115
|
-
ref: RefObject<PromptCardHandle | null>
|
|
116
|
-
onAnswer: (id: string, label: string, ok: boolean) => void
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export const MessageItem = memo(({ message, streaming, prompt, onRewind, onPick }: {
|
|
120
|
-
message: Message
|
|
121
|
-
streaming: boolean
|
|
122
|
-
prompt?: PromptWire
|
|
123
|
-
onRewind?: (m: Message) => void
|
|
124
|
-
onPick?: (m: Message) => void
|
|
125
|
-
}) => {
|
|
126
|
-
if (message.role === "system") return <SystemMessage message={message} />
|
|
127
|
-
if (message.role === "user") return <UserMessage message={message} onRewind={onRewind} />
|
|
128
|
-
return <AssistantMessage message={message} streaming={streaming} prompt={prompt} onPick={onPick} />
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const SystemMessage = memo(({ message }: { message: Message }) => {
|
|
132
|
-
const theme = useTheme().theme
|
|
133
|
-
return (
|
|
134
|
-
<box marginBottom={1}>
|
|
135
|
-
<Gutter color={theme.textMuted} glyph="·">
|
|
136
|
-
<box minHeight={1}>
|
|
137
|
-
<text fg={theme.textMuted} wrapMode="word">{extract(message)}</text>
|
|
138
|
-
</box>
|
|
139
|
-
</Gutter>
|
|
140
|
-
</box>
|
|
141
|
-
)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
const UserMessage = memo(({ message, onRewind }: { message: Message; onRewind?: (m: Message) => void }) => {
|
|
145
|
-
const theme = useTheme().theme
|
|
146
|
-
const [hover, setHover] = useState(false)
|
|
147
|
-
const click = useClick(onRewind && (() => onRewind(message)))
|
|
148
|
-
const segs = useMemo(
|
|
149
|
-
() => message.parts.map(p => p.type === "text" && p.content ? splitContent(p.content) : null),
|
|
150
|
-
[message.parts],
|
|
151
|
-
)
|
|
152
|
-
return (
|
|
153
|
-
<box
|
|
154
|
-
flexDirection="column"
|
|
155
|
-
marginBottom={1}
|
|
156
|
-
backgroundColor={hover ? theme.backgroundElement : undefined}
|
|
157
|
-
onMouseOver={() => setHover(true)}
|
|
158
|
-
onMouseOut={() => setHover(false)}
|
|
159
|
-
{...click}
|
|
160
|
-
>
|
|
161
|
-
<Gutter color={theme.primary} side="left">
|
|
162
|
-
<box minHeight={1} flexDirection="column">
|
|
163
|
-
{message.parts.map((p, i) => {
|
|
164
|
-
const seg = segs[i]
|
|
165
|
-
if (!seg) return null
|
|
166
|
-
const k = (p as TextPart).key ?? i
|
|
167
|
-
return seg.map((s, j) => {
|
|
168
|
-
if ("media" in s) {
|
|
169
|
-
const kind = classify(s.media)
|
|
170
|
-
return kind === "img" ? (
|
|
171
|
-
<box key={`${k}-m${j}`}><ChafaImage path={s.media} /></box>
|
|
172
|
-
) : (
|
|
173
|
-
<box key={`${k}-m${j}`} marginTop={1}><MediaChip path={s.media} /></box>
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
if ("code" in s) return (
|
|
177
|
-
<CodeBlock key={`${k}-c${j}`} code={s.code} lang={s.lang} />
|
|
178
|
-
)
|
|
179
|
-
return <text key={`${k}-${j}`} fg={theme.text} wrapMode="word">{s.md}</text>
|
|
180
|
-
})
|
|
181
|
-
})}
|
|
182
|
-
</box>
|
|
183
|
-
</Gutter>
|
|
184
|
-
</box>
|
|
185
|
-
)
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
const AssistantMessage = memo(({ message, streaming, prompt, onPick }: {
|
|
189
|
-
message: Message; streaming: boolean; prompt?: PromptWire; onPick?: (m: Message) => void
|
|
190
|
-
}) => {
|
|
191
|
-
const ctx = useTheme()
|
|
192
|
-
const theme = ctx.theme
|
|
193
|
-
const { agentName } = useSkin()
|
|
194
|
-
const [hover, setHover] = useState(false)
|
|
195
|
-
const click = useClick(onPick && (() => onPick(message)))
|
|
196
|
-
const err = !!message.error
|
|
197
|
-
const trail = message.parts.filter((p): p is ToolPart | PromptPart =>
|
|
198
|
-
p.type === "tool" || p.type === "prompt")
|
|
199
|
-
const diffs = trail.filter((p): p is ToolPart =>
|
|
200
|
-
p.type === "tool" && (!!p.diff || isDiff(p.result)))
|
|
201
|
-
|
|
202
|
-
// Split once per parts identity so hover (which re-renders this
|
|
203
|
-
// component) doesn't re-scan text. parts identity changes per
|
|
204
|
-
// streaming delta and stabilizes on completion.
|
|
205
|
-
const segs = useMemo(
|
|
206
|
-
() => message.parts.map(p => p.type === "text" && p.content ? splitContent(p.content) : null),
|
|
207
|
-
[message.parts],
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const header = [
|
|
211
|
-
agentName,
|
|
212
|
-
message.usage ? `${tokens(message.usage.input)}→${tokens(message.usage.output)} tok` : null,
|
|
213
|
-
message.duration ? duration(message.duration) : null,
|
|
214
|
-
].filter(Boolean).join(" · ")
|
|
215
|
-
|
|
216
|
-
const part = (p: Part, i: number) => {
|
|
217
|
-
if (p.type === "prompt") {
|
|
218
|
-
// ref only attaches to the pending card (answered cards are
|
|
219
|
-
// inert outcome rows and never receive keys).
|
|
220
|
-
return (
|
|
221
|
-
<box key={`pr-${p.id}`} marginTop={1}
|
|
222
|
-
onMouseDown={(e: MouseEvent) => e.stopPropagation()}>
|
|
223
|
-
<PromptCard part={p}
|
|
224
|
-
ref={!p.answered ? prompt?.ref : undefined}
|
|
225
|
-
onAnswer={prompt?.onAnswer ?? (() => {})} />
|
|
226
|
-
</box>
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
const seg = segs[i]
|
|
230
|
-
if (!seg) return null
|
|
231
|
-
const k = (p as TextPart).key ?? i
|
|
232
|
-
const last = streaming && (p as TextPart).streaming
|
|
233
|
-
return seg.map((s, j) => {
|
|
234
|
-
const tail = last && j === seg.length - 1
|
|
235
|
-
if ("media" in s) {
|
|
236
|
-
const kind = classify(s.media)
|
|
237
|
-
return kind === "img" ? (
|
|
238
|
-
<box key={`${k}-m${j}`}><ChafaImage path={s.media} /></box>
|
|
239
|
-
) : (
|
|
240
|
-
<box key={`${k}-m${j}`} marginTop={1}><MediaChip path={s.media} /></box>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
if ("code" in s) return (
|
|
244
|
-
<CodeBlock key={`${k}-c${j}`} code={s.code} lang={s.lang} streaming={tail} />
|
|
245
|
-
)
|
|
246
|
-
// LaTeX → Unicode. mathify scans for $…$ / \(…\) / $$…$$ / \[…\]
|
|
247
|
-
// spans and rewrites only their interiors via texToUnicode; prose
|
|
248
|
-
// like `browser_navigate` is never touched. Inline-code spans are
|
|
249
|
-
// skipped. Unknown commands inside a span pass through, so partial
|
|
250
|
-
// streaming deltas (e.g. "\al" before "\alpha" arrives) are safe —
|
|
251
|
-
// they simply don't substitute yet.
|
|
252
|
-
return (
|
|
253
|
-
<box key={`${k}-${j}`}>
|
|
254
|
-
<markdown content={mathify(s.md)} fg={theme.markdownText}
|
|
255
|
-
syntaxStyle={ctx.syntaxStyle} streaming={tail} />
|
|
256
|
-
</box>
|
|
257
|
-
)
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return (
|
|
262
|
-
<box flexDirection="column" marginBottom={1}
|
|
263
|
-
backgroundColor={hover ? theme.backgroundElement : undefined}
|
|
264
|
-
onMouseOver={() => setHover(true)}
|
|
265
|
-
onMouseOut={() => setHover(false)}
|
|
266
|
-
{...click}>
|
|
267
|
-
<Gutter color={err ? theme.error : theme.accent} side="right">
|
|
268
|
-
<box height={1} flexDirection="row">
|
|
269
|
-
<box flexGrow={1}><text fg={theme.textMuted}>{header}</text></box>
|
|
270
|
-
{trail.length ? (
|
|
271
|
-
<box><text fg={theme.textMuted}>
|
|
272
|
-
{trunc(trail.map(p => p.type === "tool" ? p.name : "?").join(" · "), 40)}
|
|
273
|
-
</text></box>
|
|
274
|
-
) : null}
|
|
275
|
-
</box>
|
|
276
|
-
{message.parts.map(part)}
|
|
277
|
-
{diffs.map(t => <InlineDiff key={t.id || t.name} tool={t} />)}
|
|
278
|
-
{err ? <ErrorBlock text={message.error!} /> : null}
|
|
279
|
-
</Gutter>
|
|
280
|
-
</box>
|
|
281
|
-
)
|
|
282
|
-
})
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { memo, useMemo, useState } from "react"
|
|
2
|
-
import { MessageItem, type PromptWire } from "./MessageItem"
|
|
3
|
-
import { TypingIndicator } from "./TypingIndicator"
|
|
4
|
-
import { useTheme } from "../../theme"
|
|
5
|
-
import { randomTip, splitTip } from "../../utils/tips"
|
|
6
|
-
import type { Message } from "../../types/message"
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
messages: Message[]
|
|
10
|
-
streaming: boolean
|
|
11
|
-
prompt?: PromptWire
|
|
12
|
-
onRewind?: (m: Message) => void
|
|
13
|
-
onPick?: (m: Message) => void
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const MessageList = memo(({ messages, streaming, prompt, onRewind, onPick }: Props) => {
|
|
17
|
-
const theme = useTheme().theme
|
|
18
|
-
|
|
19
|
-
const style = useMemo(() => ({
|
|
20
|
-
viewportOptions: { backgroundColor: theme.background },
|
|
21
|
-
scrollbarOptions: {
|
|
22
|
-
trackOptions: {
|
|
23
|
-
foregroundColor: theme.borderSubtle,
|
|
24
|
-
backgroundColor: theme.background,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
}), [theme])
|
|
28
|
-
|
|
29
|
-
if (messages.length === 0) {
|
|
30
|
-
return (
|
|
31
|
-
<box flexGrow={1} justifyContent="center" alignItems="center">
|
|
32
|
-
<box flexDirection="column" alignItems="center" gap={1}>
|
|
33
|
-
<text>
|
|
34
|
-
<span fg={theme.primary}>╭─────────────────────────╮</span>
|
|
35
|
-
</text>
|
|
36
|
-
<text>
|
|
37
|
-
<span fg={theme.primary}>│ </span>
|
|
38
|
-
<span fg={theme.accent}>H E R M</span>
|
|
39
|
-
<span fg={theme.primary}> │</span>
|
|
40
|
-
</text>
|
|
41
|
-
<text>
|
|
42
|
-
<span fg={theme.primary}>╰─────────────────────────╯</span>
|
|
43
|
-
</text>
|
|
44
|
-
<text fg={theme.textMuted}> </text>
|
|
45
|
-
<text fg={theme.textMuted}>Terminal interface for Hermes</text>
|
|
46
|
-
<text fg={theme.textMuted}>Type a message below to begin.</text>
|
|
47
|
-
<text fg={theme.textMuted}> </text>
|
|
48
|
-
<text>
|
|
49
|
-
<span fg={theme.textMuted}> Enter </span>
|
|
50
|
-
<span fg={theme.borderSubtle}>Send message</span>
|
|
51
|
-
</text>
|
|
52
|
-
<text>
|
|
53
|
-
<span fg={theme.textMuted}> Esc×2 </span>
|
|
54
|
-
<span fg={theme.borderSubtle}>Interrupt generation</span>
|
|
55
|
-
</text>
|
|
56
|
-
<text>
|
|
57
|
-
<span fg={theme.textMuted}> Ctrl+Y </span>
|
|
58
|
-
<span fg={theme.borderSubtle}>Copy last response</span>
|
|
59
|
-
</text>
|
|
60
|
-
<text>
|
|
61
|
-
<span fg={theme.textMuted}> ↑ / ↓ </span>
|
|
62
|
-
<span fg={theme.borderSubtle}>Prompt history</span>
|
|
63
|
-
</text>
|
|
64
|
-
<Tip />
|
|
65
|
-
</box>
|
|
66
|
-
</box>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const last = messages[messages.length - 1]
|
|
71
|
-
const lastStreaming = streaming && last?.role === "assistant"
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<scrollbox
|
|
75
|
-
flexGrow={1}
|
|
76
|
-
scrollY
|
|
77
|
-
stickyScroll
|
|
78
|
-
stickyStart="bottom"
|
|
79
|
-
style={style}
|
|
80
|
-
>
|
|
81
|
-
<box flexDirection="column" paddingBottom={1}>
|
|
82
|
-
{messages.map((msg, i) => (
|
|
83
|
-
<MessageItem
|
|
84
|
-
key={msg.id}
|
|
85
|
-
message={msg}
|
|
86
|
-
streaming={lastStreaming && i === messages.length - 1}
|
|
87
|
-
prompt={prompt}
|
|
88
|
-
onRewind={onRewind}
|
|
89
|
-
onPick={onPick}
|
|
90
|
-
/>
|
|
91
|
-
))}
|
|
92
|
-
{streaming && last?.role !== "assistant" && <TypingIndicator />}
|
|
93
|
-
</box>
|
|
94
|
-
</scrollbox>
|
|
95
|
-
)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
// One random Hermes CLI tip; click to cycle. Width-capped so it
|
|
99
|
-
// doesn't blow out the centered empty-state column.
|
|
100
|
-
const Tip = memo(() => {
|
|
101
|
-
const theme = useTheme().theme
|
|
102
|
-
const [tip, setTip] = useState(() => randomTip())
|
|
103
|
-
return (
|
|
104
|
-
<box flexDirection="column" alignItems="center" maxWidth={64} marginTop={1}
|
|
105
|
-
onMouseDown={() => setTip(t => randomTip(t))}>
|
|
106
|
-
<text fg={theme.borderSubtle}>─── tip ───</text>
|
|
107
|
-
<text wrapMode="word">
|
|
108
|
-
{splitTip(tip).map((p, i) =>
|
|
109
|
-
<span key={i} fg={p.hl ? theme.accent : theme.textMuted}>{p.t}</span>,
|
|
110
|
-
)}
|
|
111
|
-
</text>
|
|
112
|
-
</box>
|
|
113
|
-
)
|
|
114
|
-
})
|