hermes-agent/web/package.json
emozilla 3d21aee811 feat(web): add Chat tab with xterm.js terminal + Sessions resume button
Wires the new /api/pty WebSocket into the dashboard as a top-level
Chat tab. Clicking Chat (or the ▶ play icon on any session row)
spawns a PTY running hermes --tui and renders its ANSI output with
xterm.js in the browser.

Frontend
--------

web/src/pages/ChatPage.tsx
  * @xterm/xterm v6 + @xterm/addon-webgl renderer (pixel-perfect cell
    grid — DOM and canvas renderers each have layout artifacts that
    break box-drawing glyph connectivity in a browser)
  * @xterm/addon-fit for container-driven resize
  * @xterm/addon-unicode11 for modern wide-char widths (matches Ink's
    string-width computation so kaomoji / CJK / emoji land on the
    same cell boundaries as the host expects)
  * @xterm/addon-web-links for URL auto-linking
  * Rounded dark-teal "terminal window" container with 12px internal
    padding + drop shadow for visual identity within the dashboard
  * Clipboard wiring:
      - Ctrl/Cmd+Shift+C copies xterm selection to system clipboard
      - Ctrl/Cmd+Shift+V pastes system clipboard into the PTY
      - OSC 52 handler writes terminal-emitted clipboard sequences
        (how Ink's own Ctrl+C and /copy command deliver copy events);
        decodes via TextDecoder so multi-byte UTF-8 codepoints
        (U+2265, emoji, CJK) round-trip correctly
      - Plain Ctrl+C still passes through as SIGINT to interrupt a
        running response
  * Floating "copy last response" button in the bottom-right corner.
    Triggers Ink's /copy slash by sending bytes in two frames with a
    100ms gap — Ink's tokenizer coalesces rapid adjacent bytes into
    a paste event (bypasses the slash dispatcher), so we deliberately
    split '/copy' and '\r' into separate packets to land them as
    individual keypresses.

web/src/App.tsx
  Chat nav entry (Terminal icon) at position 2 and <Route path="/chat">.

web/src/pages/SessionsPage.tsx
  Play-icon button per session row that navigates to /chat?resume=<id>;
  the PTY bridge forwards the resume param to hermes --tui --resume.

web/src/i18n/{en,zh,types}.ts
  nav.chat label + sessions.resumeInChat action label.

web/vite.config.ts
  /api proxy gains ws: true so WebSocket upgrades forward to :9119
  when running Vite dev mode against a separate hermes dashboard
  backend.

web/src/index.css + web/public/fonts-terminal/
  Bundles JetBrains Mono (Regular/Bold/Italic, Apache-2.0, ~280 KB
  total) as a local webfont. Fonts live outside web/public/fonts/
  because the sync-assets prebuild step wipes that directory from
  @nous-research/ui every build.

Package deps
------------

Net new: @xterm/xterm ^6.0.0, @xterm/addon-fit ^0.11.0,
         @xterm/addon-webgl ^0.19.0, @xterm/addon-unicode11 ^0.9.0,
         @xterm/addon-web-links ^0.12.0.

Bundle impact: +420 KB minified / +105 KB gzipped. Acceptable for a
feature that replaces what would otherwise be a rewrite of the entire
TUI surface in React.

Backend contract preserved
---------------------------

Every TUI affordance (slash popover, model picker, tool cards,
markdown streaming, clarify/sudo/approval prompts, skin engine, wide
chars, mouse tracking) lands in the browser unchanged because we are
running the real Ink binary. Adding a feature to the TUI surfaces in
the dashboard immediately. Do NOT add parallel React chat surfaces.
2026-04-21 03:10:30 -04:00

51 lines
1.5 KiB
JSON

{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"sync-assets": "rm -rf public/fonts public/ds-assets && cp -r node_modules/@nous-research/ui/dist/fonts public/fonts && cp -r node_modules/@nous-research/ui/dist/assets public/ds-assets",
"predev": "npm run sync-assets",
"prebuild": "npm run sync-assets",
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@nous-research/ui": "^0.3.0",
"@observablehq/plot": "^0.6.17",
"@react-three/fiber": "^9.6.0",
"@tailwindcss/vite": "^4.2.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gsap": "^3.15.0",
"leva": "^0.10.1",
"lucide-react": "^0.577.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"three": "^0.180.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1"
}
}