mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 08:47:26 +08:00
feat(tui): add scripted showroom demos
This commit is contained in:
46
ui-tui/.showroom/README.md
Normal file
46
ui-tui/.showroom/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# TUI Showroom
|
||||
|
||||
Scripted, record-ready demos for `ui-tui`.
|
||||
|
||||
```bash
|
||||
npm run showroom
|
||||
npm run showroom:build
|
||||
npm run showroom:type-check
|
||||
```
|
||||
|
||||
`npm run showroom` serves the default workflow at `http://127.0.0.1:4317`.
|
||||
|
||||
```bash
|
||||
npm run showroom -- --workflow .showroom/workflows/feature-tour.json --port 4318
|
||||
npm run showroom:build -- .showroom/workflows/feature-tour.json .showroom/dist/feature-tour.html
|
||||
```
|
||||
|
||||
## Workflow Shape
|
||||
|
||||
Workflows are JSON so the renderer has no extra deps.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Hermes TUI Feature Tour",
|
||||
"viewport": { "cols": 96, "rows": 30, "scale": 4 },
|
||||
"timeline": [
|
||||
{ "at": 0, "type": "status", "text": "summoning hermes..." },
|
||||
{ "at": 250, "type": "message", "id": "prompt", "role": "user", "text": "Build a plan." },
|
||||
{ "at": 900, "type": "caption", "target": "prompt", "text": "Named targets drive overlays." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Timeline Actions
|
||||
|
||||
- `status`: set top status text, with optional `detail`
|
||||
- `compose`: type into the composer
|
||||
- `message`: append a transcript line; supports `role`, `id`, `text`, `duration`
|
||||
- `tool`: append a tool activity card; supports `id`, `title`, `items`
|
||||
- `caption`: fade in a caption near `target`; supports `position`, `duration`
|
||||
- `spotlight`: draw a spotlight around `target`; supports `pad`, `duration`
|
||||
- `highlight`: temporarily emphasize `target`
|
||||
- `fade`: set `target` opacity over `duration`
|
||||
- `clear`: reset transcript and overlays
|
||||
|
||||
Targets are `id` values from `message`, `tool`, and captions. The stage is rendered at `viewport.scale`, so `scale: 4` creates a 4x capture surface without changing the source terminal proportions.
|
||||
12
ui-tui/.showroom/build.ts
Normal file
12
ui-tui/.showroom/build.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
|
||||
import { defaultWorkflowPath, readWorkflow, renderPage, showroomRoot } from './page.js'
|
||||
|
||||
const workflowPath = resolve(process.cwd(), process.argv[2] ?? defaultWorkflowPath)
|
||||
const outPath = resolve(process.cwd(), process.argv[3] ?? join(showroomRoot, 'dist', 'index.html'))
|
||||
|
||||
mkdirSync(dirname(outPath), { recursive: true })
|
||||
writeFileSync(outPath, renderPage(readWorkflow(workflowPath)))
|
||||
|
||||
console.log(outPath)
|
||||
29
ui-tui/.showroom/page.ts
Normal file
29
ui-tui/.showroom/page.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export const showroomRoot = dirname(fileURLToPath(import.meta.url))
|
||||
export const defaultWorkflowPath = join(showroomRoot, 'workflows', 'feature-tour.json')
|
||||
|
||||
export const readWorkflow = (path = defaultWorkflowPath) => JSON.parse(readFileSync(path, 'utf8'))
|
||||
|
||||
export const renderPage = (workflow: unknown) => {
|
||||
const css = readFileSync(join(showroomRoot, 'src', 'showroom.css'), 'utf8')
|
||||
const js = readFileSync(join(showroomRoot, 'src', 'showroom.js'), 'utf8')
|
||||
const data = JSON.stringify(workflow).replace(/</g, '\\u003c')
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Hermes TUI Showroom</title>
|
||||
<style>${css}</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="showroom"></main>
|
||||
<script>window.__SHOWROOM_WORKFLOW__ = ${data}</script>
|
||||
<script type="module">${js}</script>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
36
ui-tui/.showroom/server.ts
Normal file
36
ui-tui/.showroom/server.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createServer } from 'node:http'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { defaultWorkflowPath, readWorkflow, renderPage } from './page.js'
|
||||
|
||||
const arg = (name: string) => {
|
||||
const index = process.argv.indexOf(name)
|
||||
|
||||
return index === -1 ? undefined : process.argv[index + 1]
|
||||
}
|
||||
|
||||
const port = Number(arg('--port') ?? process.env.PORT ?? 4317)
|
||||
const workflowPath = resolve(process.cwd(), arg('--workflow') ?? process.argv[2] ?? defaultWorkflowPath)
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
if (req.url === '/healthz') {
|
||||
res.writeHead(200).end('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const page = renderPage(readWorkflow(workflowPath))
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }).end(page)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }).end(message)
|
||||
}
|
||||
})
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`showroom: http://127.0.0.1:${port}`)
|
||||
console.log(`workflow: ${workflowPath}`)
|
||||
})
|
||||
219
ui-tui/.showroom/src/showroom.css
Normal file
219
ui-tui/.showroom/src/showroom.css
Normal file
@@ -0,0 +1,219 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
background: #070707;
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, rgba(214, 168, 79, 0.18), transparent 34rem),
|
||||
radial-gradient(circle at 82% 18%, rgba(90, 130, 255, 0.14), transparent 30rem), #050505;
|
||||
}
|
||||
|
||||
#showroom {
|
||||
min-height: 100vh;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.showroom-shell {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.showroom-title {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
color: #f5e8c7;
|
||||
font-size: 20px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.showroom-meta {
|
||||
color: #8f856f;
|
||||
font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.showroom-stage {
|
||||
position: relative;
|
||||
width: var(--stage-w);
|
||||
height: var(--stage-h);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(245, 232, 199, 0.18);
|
||||
border-radius: 28px;
|
||||
background: #080808;
|
||||
box-shadow:
|
||||
0 48px 160px rgba(0, 0, 0, 0.56),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.035) inset;
|
||||
}
|
||||
|
||||
.showroom-terminal {
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
width: var(--term-w);
|
||||
height: var(--term-h);
|
||||
transform: scale(var(--scale));
|
||||
transform-origin: top left;
|
||||
overflow: hidden;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 18%), #0a0a0a;
|
||||
color: #d8d0bd;
|
||||
font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.showroom-status,
|
||||
.showroom-composer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
color: #8f856f;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.showroom-status {
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(245, 232, 199, 0.1);
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.showroom-composer {
|
||||
border-top: 1px solid rgba(245, 232, 199, 0.1);
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.showroom-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
overflow: hidden;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.showroom-line,
|
||||
.showroom-tool {
|
||||
transition:
|
||||
opacity 420ms ease,
|
||||
filter 420ms ease,
|
||||
transform 420ms ease,
|
||||
background 420ms ease;
|
||||
}
|
||||
|
||||
.showroom-line {
|
||||
display: grid;
|
||||
grid-template-columns: 26px 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.showroom-glyph {
|
||||
color: var(--role);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.showroom-copy {
|
||||
color: var(--role);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.showroom-line-assistant .showroom-copy {
|
||||
color: #d8d0bd;
|
||||
}
|
||||
|
||||
.showroom-tool {
|
||||
margin-left: 32px;
|
||||
border: 1px solid rgba(214, 168, 79, 0.18);
|
||||
border-radius: 11px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(214, 168, 79, 0.055);
|
||||
color: #c7b891;
|
||||
}
|
||||
|
||||
.showroom-tool-title {
|
||||
color: #f1cb78;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.showroom-tool-items {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
margin-top: 5px;
|
||||
color: #908872;
|
||||
}
|
||||
|
||||
.is-highlighted {
|
||||
filter: brightness(1.45);
|
||||
background: rgba(214, 168, 79, 0.12);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.showroom-overlays {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.showroom-caption,
|
||||
.showroom-spotlight {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 360ms ease,
|
||||
transform 360ms ease;
|
||||
}
|
||||
|
||||
.showroom-caption {
|
||||
max-width: 420px;
|
||||
border: 1px solid rgba(245, 232, 199, 0.2);
|
||||
border-radius: 18px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(12, 12, 12, 0.82);
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.42);
|
||||
color: #f5e8c7;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
.showroom-spotlight {
|
||||
border: 2px solid rgba(241, 203, 120, 0.7);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 0 0 9999px rgba(0, 0, 0, 0.34),
|
||||
0 0 40px rgba(241, 203, 120, 0.22);
|
||||
}
|
||||
|
||||
.showroom-caption.is-visible,
|
||||
.showroom-spotlight.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.showroom-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.showroom-controls button {
|
||||
border: 1px solid rgba(245, 232, 199, 0.18);
|
||||
border-radius: 999px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(245, 232, 199, 0.06);
|
||||
color: #f5e8c7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.showroom-controls button:hover {
|
||||
background: rgba(245, 232, 199, 0.12);
|
||||
}
|
||||
302
ui-tui/.showroom/src/showroom.js
Normal file
302
ui-tui/.showroom/src/showroom.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const workflow = window.__SHOWROOM_WORKFLOW__
|
||||
const root = document.getElementById('showroom')
|
||||
const timers = []
|
||||
|
||||
let body
|
||||
let composer
|
||||
let overlays
|
||||
let statusLeft
|
||||
let statusRight
|
||||
let viewportConfig
|
||||
|
||||
const role = {
|
||||
assistant: { color: '#d8d0bd', glyph: '✦' },
|
||||
system: { color: '#8f856f', glyph: '·' },
|
||||
tool: { color: '#f1cb78', glyph: '┊' },
|
||||
user: { color: '#f1cb78', glyph: '›' }
|
||||
}
|
||||
|
||||
const escapeHtml = value =>
|
||||
String(value ?? '').replace(
|
||||
/[&<>"']/g,
|
||||
char =>
|
||||
({
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
})[char]
|
||||
)
|
||||
|
||||
const clearTimers = () => {
|
||||
while (timers.length) {
|
||||
clearTimeout(timers.pop())
|
||||
}
|
||||
}
|
||||
|
||||
const target = id => (id ? document.querySelector(`[data-target="${CSS.escape(id)}"]`) : null)
|
||||
|
||||
const setText = (node, text = '', duration = 0) => {
|
||||
if (!duration) {
|
||||
node.textContent = text
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const chars = [...text]
|
||||
const started = performance.now()
|
||||
|
||||
const frame = now => {
|
||||
const n = Math.min(chars.length, Math.ceil(((now - started) / duration) * chars.length))
|
||||
node.textContent = chars.slice(0, n).join('')
|
||||
|
||||
if (n < chars.length) {
|
||||
requestAnimationFrame(frame)
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame)
|
||||
}
|
||||
|
||||
const removeAfter = (node, duration = 1400) => {
|
||||
timers.push(
|
||||
setTimeout(() => {
|
||||
node.classList.remove('is-visible')
|
||||
timers.push(setTimeout(() => node.remove(), 420))
|
||||
}, duration)
|
||||
)
|
||||
}
|
||||
|
||||
const rectFor = (id, pad = 10) => {
|
||||
const el = target(id)
|
||||
|
||||
if (!el) {
|
||||
return null
|
||||
}
|
||||
|
||||
const stage = overlays.getBoundingClientRect()
|
||||
const rect = el.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
height: rect.height + pad * 2,
|
||||
left: rect.left - stage.left - pad,
|
||||
top: rect.top - stage.top - pad,
|
||||
width: rect.width + pad * 2
|
||||
}
|
||||
}
|
||||
|
||||
const placeNear = (node, id, position = 'right') => {
|
||||
const rect = rectFor(id, 0)
|
||||
|
||||
if (!rect) {
|
||||
node.style.left = `${viewportConfig.scale * 28}px`
|
||||
node.style.top = `${viewportConfig.scale * 28}px`
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const gap = 24
|
||||
const left = position === 'left' ? rect.left - node.offsetWidth - gap : rect.left + rect.width + gap
|
||||
const top = position === 'top' ? rect.top - node.offsetHeight - gap : rect.top
|
||||
|
||||
node.style.left = `${Math.max(18, left)}px`
|
||||
node.style.top = `${Math.max(18, top)}px`
|
||||
}
|
||||
|
||||
const message = action => {
|
||||
const spec = role[action.role] ?? role.assistant
|
||||
const line = document.createElement('div')
|
||||
const glyph = document.createElement('span')
|
||||
const copy = document.createElement('div')
|
||||
|
||||
line.className = `showroom-line showroom-line-${action.role ?? 'assistant'}`
|
||||
line.dataset.target = action.id ?? ''
|
||||
line.style.setProperty('--role', spec.color)
|
||||
|
||||
glyph.className = 'showroom-glyph'
|
||||
glyph.textContent = spec.glyph
|
||||
|
||||
copy.className = 'showroom-copy'
|
||||
|
||||
line.append(glyph, copy)
|
||||
body.append(line)
|
||||
setText(copy, action.text, action.duration)
|
||||
}
|
||||
|
||||
const tool = action => {
|
||||
const box = document.createElement('div')
|
||||
const title = document.createElement('div')
|
||||
const items = document.createElement('div')
|
||||
|
||||
box.className = 'showroom-tool'
|
||||
box.dataset.target = action.id ?? ''
|
||||
|
||||
title.className = 'showroom-tool-title'
|
||||
title.textContent = action.title ?? 'tool activity'
|
||||
|
||||
items.className = 'showroom-tool-items'
|
||||
|
||||
for (const item of action.items ?? []) {
|
||||
const row = document.createElement('div')
|
||||
|
||||
row.textContent = item
|
||||
items.append(row)
|
||||
}
|
||||
|
||||
box.append(title, items)
|
||||
body.append(box)
|
||||
}
|
||||
|
||||
const fade = action => {
|
||||
const el = target(action.target)
|
||||
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
|
||||
el.style.transition = `opacity ${action.duration ?? 420}ms ease`
|
||||
el.style.opacity = String(action.to ?? 0)
|
||||
}
|
||||
|
||||
const highlight = action => {
|
||||
const el = target(action.target)
|
||||
|
||||
if (!el) {
|
||||
return
|
||||
}
|
||||
|
||||
el.classList.add('is-highlighted')
|
||||
timers.push(setTimeout(() => el.classList.remove('is-highlighted'), action.duration ?? 1200))
|
||||
}
|
||||
|
||||
const caption = action => {
|
||||
const node = document.createElement('div')
|
||||
|
||||
node.className = 'showroom-caption'
|
||||
node.dataset.target = action.id ?? ''
|
||||
node.textContent = action.text ?? ''
|
||||
overlays.append(node)
|
||||
placeNear(node, action.target, action.position)
|
||||
requestAnimationFrame(() => node.classList.add('is-visible'))
|
||||
removeAfter(node, action.duration)
|
||||
}
|
||||
|
||||
const spotlight = action => {
|
||||
const rect = rectFor(action.target, action.pad ?? 10)
|
||||
|
||||
if (!rect) {
|
||||
return
|
||||
}
|
||||
|
||||
const node = document.createElement('div')
|
||||
|
||||
node.className = 'showroom-spotlight'
|
||||
node.style.left = `${rect.left}px`
|
||||
node.style.top = `${rect.top}px`
|
||||
node.style.width = `${rect.width}px`
|
||||
node.style.height = `${rect.height}px`
|
||||
overlays.append(node)
|
||||
requestAnimationFrame(() => node.classList.add('is-visible'))
|
||||
removeAfter(node, action.duration)
|
||||
}
|
||||
|
||||
const status = action => {
|
||||
statusLeft.textContent = action.text ?? ''
|
||||
statusRight.textContent = action.detail ?? ''
|
||||
}
|
||||
|
||||
const compose = action => setText(composer, action.text ?? '', action.duration)
|
||||
|
||||
const clear = () => {
|
||||
body.textContent = ''
|
||||
overlays.textContent = ''
|
||||
}
|
||||
|
||||
const run = action =>
|
||||
({
|
||||
caption,
|
||||
clear,
|
||||
compose,
|
||||
fade,
|
||||
highlight,
|
||||
message,
|
||||
spotlight,
|
||||
status,
|
||||
tool
|
||||
})[action.type]?.(action)
|
||||
|
||||
const play = () => {
|
||||
clearTimers()
|
||||
clear()
|
||||
statusLeft.textContent = ''
|
||||
statusRight.textContent = ''
|
||||
composer.textContent = workflow.composer ?? '›'
|
||||
|
||||
for (const action of [...(workflow.timeline ?? [])].sort((a, b) => a.at - b.at)) {
|
||||
timers.push(setTimeout(() => run(action), action.at))
|
||||
}
|
||||
}
|
||||
|
||||
const mount = () => {
|
||||
const viewport = { cellWidth: 9, cols: 96, lineHeight: 18, rows: 30, scale: 4, ...workflow.viewport }
|
||||
const shell = document.createElement('section')
|
||||
|
||||
viewportConfig = viewport
|
||||
shell.className = 'showroom-shell'
|
||||
shell.style.setProperty('--cell-w', `${viewport.cellWidth}px`)
|
||||
shell.style.setProperty('--cols', `${viewport.cols}`)
|
||||
shell.style.setProperty('--line-h', `${viewport.lineHeight}px`)
|
||||
shell.style.setProperty('--rows', `${viewport.rows}`)
|
||||
shell.style.setProperty('--scale', `${viewport.scale}`)
|
||||
shell.style.setProperty('--stage-h', `${viewport.rows * viewport.lineHeight * viewport.scale}px`)
|
||||
shell.style.setProperty('--stage-w', `${viewport.cols * viewport.cellWidth * viewport.scale}px`)
|
||||
shell.style.setProperty('--term-h', `${viewport.rows * viewport.lineHeight}px`)
|
||||
shell.style.setProperty('--term-w', `${viewport.cols * viewport.cellWidth}px`)
|
||||
|
||||
shell.innerHTML = `
|
||||
<header class="showroom-title">
|
||||
<span>${escapeHtml(workflow.title ?? 'Hermes TUI Showroom')}</span>
|
||||
<span class="showroom-meta">${viewport.cols}x${viewport.rows} · ${viewport.scale}x</span>
|
||||
</header>
|
||||
<div class="showroom-stage">
|
||||
<div class="showroom-terminal">
|
||||
<div class="showroom-status" data-target="status">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="showroom-body"></div>
|
||||
<div class="showroom-composer" data-target="composer"></div>
|
||||
</div>
|
||||
<div class="showroom-overlays"></div>
|
||||
</div>
|
||||
<footer class="showroom-controls">
|
||||
<button type="button" data-action="restart">Restart</button>
|
||||
<button type="button" data-action="clear">Clear</button>
|
||||
</footer>
|
||||
`
|
||||
|
||||
root.replaceChildren(shell)
|
||||
|
||||
body = shell.querySelector('.showroom-body')
|
||||
composer = shell.querySelector('.showroom-composer')
|
||||
overlays = shell.querySelector('.showroom-overlays')
|
||||
statusLeft = shell.querySelector('.showroom-status span:first-child')
|
||||
statusRight = shell.querySelector('.showroom-status span:last-child')
|
||||
|
||||
shell.querySelector('[data-action="restart"]').addEventListener('click', play)
|
||||
shell.querySelector('[data-action="clear"]').addEventListener('click', () => {
|
||||
clearTimers()
|
||||
clear()
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', event => {
|
||||
if (event.key.toLowerCase() === 'r') {
|
||||
play()
|
||||
}
|
||||
})
|
||||
|
||||
play()
|
||||
}
|
||||
|
||||
mount()
|
||||
9
ui-tui/.showroom/tsconfig.json
Normal file
9
ui-tui/.showroom/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"rootDir": ".",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
101
ui-tui/.showroom/workflows/feature-tour.json
Normal file
101
ui-tui/.showroom/workflows/feature-tour.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"title": "Hermes TUI Feature Tour",
|
||||
"composer": "› ask hermes anything",
|
||||
"viewport": {
|
||||
"cellWidth": 9,
|
||||
"cols": 96,
|
||||
"lineHeight": 18,
|
||||
"rows": 30,
|
||||
"scale": 4
|
||||
},
|
||||
"timeline": [
|
||||
{
|
||||
"at": 0,
|
||||
"detail": "showroom mode",
|
||||
"text": "summoning hermes...",
|
||||
"type": "status"
|
||||
},
|
||||
{
|
||||
"at": 250,
|
||||
"duration": 650,
|
||||
"id": "prompt",
|
||||
"role": "user",
|
||||
"text": "Build a focused plan for a safer gateway approval flow.",
|
||||
"type": "message"
|
||||
},
|
||||
{
|
||||
"at": 1050,
|
||||
"duration": 950,
|
||||
"id": "assistant-plan",
|
||||
"role": "assistant",
|
||||
"text": "I’ll trace the gateway guards first, then patch the smallest boundary that keeps approval commands live while an agent is blocked.",
|
||||
"type": "message"
|
||||
},
|
||||
{
|
||||
"at": 2180,
|
||||
"id": "tool-trail",
|
||||
"items": [
|
||||
"rg \"approval.request\" gateway/ tui_gateway/",
|
||||
"ReadFile gateway/run.py",
|
||||
"ReadFile gateway/platforms/base.py"
|
||||
],
|
||||
"title": "tool trail",
|
||||
"type": "tool"
|
||||
},
|
||||
{
|
||||
"at": 2500,
|
||||
"duration": 1500,
|
||||
"target": "tool-trail",
|
||||
"type": "spotlight"
|
||||
},
|
||||
{
|
||||
"at": 2680,
|
||||
"duration": 1600,
|
||||
"position": "right",
|
||||
"target": "tool-trail",
|
||||
"text": "Tool activity is scripted as named targets, so captions and fades can follow the exact beat.",
|
||||
"type": "caption"
|
||||
},
|
||||
{
|
||||
"at": 4450,
|
||||
"duration": 500,
|
||||
"target": "tool-trail",
|
||||
"to": 0.22,
|
||||
"type": "fade"
|
||||
},
|
||||
{
|
||||
"at": 5050,
|
||||
"duration": 700,
|
||||
"id": "assistant-result",
|
||||
"role": "assistant",
|
||||
"text": "Found the split guard. The fix is to bypass both queues only for approval/control commands and leave normal chat ordering untouched.",
|
||||
"type": "message"
|
||||
},
|
||||
{
|
||||
"at": 6050,
|
||||
"duration": 1300,
|
||||
"target": "assistant-result",
|
||||
"type": "highlight"
|
||||
},
|
||||
{
|
||||
"at": 6300,
|
||||
"duration": 1700,
|
||||
"position": "right",
|
||||
"target": "assistant-result",
|
||||
"text": "Highlights, captions, opacity fades, and spotlight boxes are all timeline actions.",
|
||||
"type": "caption"
|
||||
},
|
||||
{
|
||||
"at": 8300,
|
||||
"duration": 700,
|
||||
"text": "› /approve",
|
||||
"type": "compose"
|
||||
},
|
||||
{
|
||||
"at": 9400,
|
||||
"detail": "record-ready at 4x",
|
||||
"text": "session complete",
|
||||
"type": "status"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,7 +13,10 @@
|
||||
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'packages/**/*.{ts,tsx}'",
|
||||
"fix": "npm run lint:fix && npm run fmt",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"showroom": "tsx .showroom/server.ts",
|
||||
"showroom:build": "tsx .showroom/build.ts",
|
||||
"showroom:type-check": "tsc --noEmit -p .showroom/tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hermes/ink": "file:./packages/hermes-ink",
|
||||
|
||||
Reference in New Issue
Block a user