Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

feat: add web interface served from docker container

Aperta
#1,182 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
javascript, playwright, tailwindcss, vite

Direzione di ricerca

Start with index.js and the existing TUI entry points named in the proposal, then inspect src/tui/conversationArea.js, skillsPanel.js, memoryPanel.js, sessionsPanel.js, projectsPanel.js, settingsPanel.js, and src/provider/tokenBudget.js. Review tests/integration/api.test.js before designing the Fastify routes in src/web/server.js and the Vite frontend in web/. Done means the documented API, six views, streaming behavior, silent skill execution, and unit, integration, and Playwright tests are implemented.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

feature

Summary

Add a web mode to madz that serves a responsive browser interface from the Docker container, exposing the same core engine the TUI uses — chat, slash commands, skills, and token rate limiting — via a Fastify server and a Vite + Tailwind frontend. The API surface is a 1:1 mirror of every banner/help command, and includes a dedicated silent skill-execution endpoint.

Motivation

The core engine is currently only reachable through the Ink TUI, which requires an interactive terminal session. A web interface lets the service run independently of any TUI instance — the container can serve the app over HTTP while multiple clients connect. Sessions are already persisted to disk and resumable, so the web layer is a new transport over the existing engine rather than a new capability.

The API must be a 1:1 mirror of the TUI's command surface, not a chat-only transport. Every command in the banner and /help needs its own endpoint so the web UI can replicate the full TUI functionality. Skill execution must be a true silent invocation — dispatched by name without injecting a synthesized message into the conversation context window.

Proposed Solution

Add a --mode web (or --serve) branch to index.js that reuses the already-initialized agent, sessionState, and dispatchProvider instead of rendering the TUI. The web layer consists of:

  • Fastify server (src/web/server.js) — one endpoint per TUI command (see API Surface below).
  • Vite + Tailwind frontend (web/) — a responsive single-page chat UI mirroring the TUI's features: message bubbles, reasoning side-channel, tool call display, slash commands, skills panel, and token rate-limit indicator.
  • SSE streaming — maps 1:1 to the existing dispatchProvider streaming callback (message, reasoning, on_tool_start, on_tool_end).

Sessions remain stored on disk and resumable from the TUI — no change to the session model. Local-only, no auth.

API Surface (session-scoped)

The session is the resource. All conversation happens within a session — a message cannot exist outside one. The session ID lives in the URL path. The client holds the session ID (from POST /api/sessions when creating, or GET /api/sessions when resuming) and sends every message to that session.

Sessions:

Endpoint Description
POST /api/sessions Create a new session → { id }
GET /api/sessions List sessions → [{ id, topic, updatedAt }]
GET /api/sessions/:id Get a session (frontmatter + messages) → { id, messages: [{ role, content, timestamp, reasoningContent? }] }

Messages (nested under a session):

Endpoint Description
POST /api/sessions/:id/messages Send a message to the session, stream SSE back. This is the chat.
POST /api/sessions/:id/interrupt Abort the in-flight stream for this session

How a conversation happens:

  1. Client calls POST /api/sessions → gets { id }.
  2. Client calls POST /api/sessions/:id/messages with { content } → server loads that session's conversation into the session state, dispatches via dispatchProvider, and streams SSE events (message, reasoning, on_tool_start, on_tool_end). The exchange is appended to the session and persisted.
  3. To continue, client calls the same POST /api/sessions/:id/messages again — the session ID carries the context.
  4. To interrupt, client calls POST /api/sessions/:id/interrupt — aborts that session's in-flight stream.

Global (not session-scoped) commands:

TUI Endpoint Description
/config set <path> <value> GET /api/config, PATCH /api/config Read/update config
/gc [status] POST /api/gc, GET /api/gc/status Run GC / show status
/help GET /api/help Command help
/memories GET /api/memories Memories panel data
/provider [set <name>] GET /api/provider, PATCH /api/provider List/switch provider
/projects GET /api/projects, PATCH /api/projects/select Projects panel data / set active
/projects clear PATCH /api/projects/clear Clear active project
/quit, /exit POST /api/quit Exit the app
/schedule [list|pause|resume|run-now] GET /api/schedule, PATCH /api/schedule/:action Manage schedules
/settings GET /api/settings Settings panel data
/skills GET /api/skills Skills panel data
/skillName [args] POST /api/skills/:name/execute Execute a skill by name (silent)

Token rate limit:

TUI Endpoint Description
Status bar token counter GET /api/tokens Live rolling token usage + budget ceiling

Skill execution (silent):

POST /api/skills/:name/execute — invoke a skill by name WITHOUT injecting a synthesized user message into the context window. This is distinct from the current TUI /skill path, which synthesizes "Run the <skill> skill" and calls sessionState.addExchange({ role: "user", content: ... }) — polluting the conversation context. The silent path dispatches the skill directly.

SPA Specification

The frontend is a single-page app built with Vite + Tailwind, served statically by @fastify/static. It must replicate the TUI's six views. Each view has a defined data contract from the API.

View 1 — Conversation (default)
  • Message bubbles — user/assistant/system roles, streaming state, timestamps.
  • Reasoning side-channel — collapsible 💭 panel for reasoning segments, rendered offset so it does not fragment the message flow.
  • Tool call display — on_tool_start/on_tool_end/tool_result/on_tool_error events rendered as inline tool-call chips (name + status), matching createStreamingHandler in src/tui/conversationArea.js:591-700.
  • Markdown rendering — assistant content rendered as markdown (the TUI uses marked via src/tui/markdownText.js).
  • Slash command input — / triggers command autocomplete; unknown /skillName routes to POST /api/skills/:name/execute.
  • Interrupt — a stop button that calls POST /api/sessions/:id/interrupt.
  • Data source: POST /api/sessions/:id/messages (SSE), GET /api/sessions/:id.
View 2 — Skills panel
  • List registered skills with name + description (truncated at 500 chars, matching DESCRIPTION_MAX_LENGTH in src/tui/skillsPanel.js).
  • Select a skill → invoke via POST /api/skills/:name/execute.
  • Data source: GET /api/skills → { skills: [{ name, description, location }] } (from registry.getCatalog()).
View 3 — Memory panel
  • Browse canonical user memories from memory/context/, excluding ephemeral-*.md, reflection.md, clarifications.md (matching src/tui/memoryPanel.js).
  • Select an entry → view its full content.
  • Data source: GET /api/memories → { entries: [{ id, title, content, updatedAt }] }.
View 4 — Settings panel
  • Read-only config section browser with collapsible groups (matching src/tui/settingsPanel.js).
  • Flatten nested config into { path, value } pairs.
  • Data source: GET /api/settings → { sections: [{ name, entries: [{ path, value }] }] }.
View 5 — Sessions panel
  • List past sessions from memory/sessions/, synthesizing a topic from the first user message (matching src/tui/sessionsPanel.js).
  • Select a session → resume it.
  • Data source: GET /api/sessions → { sessions: [{ id, topic, updatedAt }] }; GET /api/sessions/:id → { id, messages: [{ role, content, timestamp, reasoningContent? }] }.
View 6 — Projects panel
  • List directories under projects/, excluding *.worktrees (matching src/tui/projectsPanel.js).
  • Select a project → set active; clear → reset.
  • Data source: GET /api/projects → { projects: [{ name, path }] }; PATCH /api/projects/select; PATCH /api/projects/clear.
Status bar
  • Model name, skill count, message count, context size, token usage, version, active project.
  • Token rate-limit indicator: GET /api/tokens → { current, maxTokensMinute } (live rolling usage from the shared TokenBudget + the configured ceiling). The TUI reads the ceiling from config.providers[].rateLimit.maxTokensMinute (src/tui/app.js:60) and the live usage from budget.current() (src/provider/tokenBudget.js).
Responsive layout
  • Mobile: single column, conversation-first, panels as drawers.
  • Desktop: conversation + collapsible side panel.

Dependencies

  • fastify (v5.12.5 — high-performance, low-overhead HTTP framework with first-class SSE support)
  • vite (v8.3.1 — fast dev server and build tooling for the frontend)
  • tailwindcss (v4.3.3 — utility-first CSS framework for responsive styling)
  • @fastify/static (v10.1.5 — serve the built frontend assets)

Testing Strategy

A mix of unit, integration, and end-to-end browser tests using Playwright.

  • Unit tests (tests/unit/web/): Verify each Fastify route handler in isolation — request validation (Zod schemas), SSE event framing, interrupt semantics, and the silent skill-execution path (assert no user message is added to session state). Mock the provider and session store so no network or disk I/O is required.
  • Integration tests (tests/integration/web/): Start the Fastify server in-process, POST a chat message, assert the SSE stream emits message/reasoning/tool events and the response is persisted to the session store. POST a skill execution and assert it runs without polluting the conversation context. Use node:test with assert to match the existing suite (tests/integration/api.test.js).
  • Playwright E2E tests (tests/web/): Launch the built frontend against the running server and exercise the real UI in a browser:
    • Conversation view — send a message, assert streaming message bubbles, reasoning side-channel, and tool-call chips render.
    • Slash command input — / autocomplete, unknown /skillName routes to skill execution.
    • Skills panel — list renders, select invokes a skill.
    • Memory / Sessions / Projects / Settings panels — each renders from its API contract.
    • Token rate-limit indicator — reflects live GET /api/tokens usage.
    • Responsive layout — assert the mobile single-column and desktop side-panel breakpoints.
  • Edge cases: Empty message, unknown sessionId, unknown skill name, concurrent streams, interrupt mid-stream, large payloads, and browser-level failures (network drop mid-SSE, rapid interrupt).

Security Considerations

  • Local-only binding: The server binds to 127.0.0.1 by default (no auth, per requirements). Document that exposing the port to the network requires adding auth — the container runs as madz with filesystem write, process spawn, and network outbound.
  • Input validation: Validate all request bodies against Zod schemas before processing (per AGENTS.md 1.2).
  • OWASP: No secrets in the web layer; all credentials remain in process.env/config. URL allowlist for any outbound requests.

Alternatives Considered

  • WebSocket — rejected: SSE is one-directional and maps 1:1 to the existing streaming callback; an explicit POST /interrupt covers abort without a bidirectional dependency.
  • Express/Hono — rejected in favor of Fastify for its performance and first-class SSE support.
  • Reusing the TUI's React components — rejected: the TUI uses Ink (terminal rendering), which is not browser-compatible; the frontend is a reimplementation of the same visual language.
  • Chat-only API — rejected: a /api/chat + /api/interrupt + /api/session trio is a chat transport, not a 1:1 mirror of the TUI. It cannot replicate slash commands, panels, or silent skill invocation.

OpenSpec Note

This project uses OpenSpec for feature development. If this request is approved, I will:

  1. Run /opsx:propose to generate a full proposal with specs and tasks
  2. Iterate on the design before any code is written
  3. Follow the task-driven implementation workflow

Additional Context

The engine is already headless — dispatchProvider(message, sessionState, streamingCallback, signal) is the exact entry point the TUI uses and emits structured streaming chunks. The web layer is a transport adapter over it. The container currently runs SSH + cron and the Ink TUI; there is no HTTP server in the codebase today.

Audit Findings (for Issue #1182)

  • index.js:303 — dispatchProvider(message, _sessionState, streamingCallback, signal) is the exact entry point the TUI uses. It calls callProvider (line 177), which emits structured streaming chunks: { type: "message", text }, { type: "reasoning", text }, { type: "on_tool_start", name, data: { input } }, { type: "on_tool_end", name, data: { output } }. This maps 1:1 onto SSE — no translation layer needed.
  • index.js:431 — exports config, sessionState, dispatchProvider, handleConversation, scheduleManager, setConfigValue, loadContext, readMemoryFile, registry, tracer, handleShutdown. All the pieces a web server needs are already exported.
  • index.js:330 — mode branch: const mode = parsed.mode === "interactive" ? "interactive" : "chat";. Add a web mode here that reuses the already-initialized agent, sessionState, and dispatchProvider instead of rendering the TUI. All subsystem init (config, telemetry, scheduler, skills, memory, GC, checkpointer) happens before this branch, so web mode is a second consumer, not a refactor.
  • src/session/index.js — exports createSession, SessionStateManager, loadSession, saveSession, createCheckpointer, handleShutdown. Session persistence is already on disk and resumable; the web layer reuses it unchanged.
  • src/config/loader.js — exports loadConfig, setConfigValue, saveConfig. A new web config section (host, port, static dir) needs a schema in src/config/schemas/ and a config.yaml entry.
  • src/tui/commandHelp.js — COMMAND_GROUPS is the single source of truth for the banner and /help. The API surface must mirror every item here: /clear, /config set, /gc, /help, /memories, /new, /provider, /projects, /projects clear, /quit, /exit, /schedule, /sessions, /settings, /skills, plus skill invocation.
  • src/tui/commandParser.js — CommandParser dispatches each command. The web layer should reuse the same dispatch logic (or a shared command module) so behavior is identical between TUI and web.
  • src/tui/conversationArea.js:300-307 — the /skill path synthesizes "Run the <skill> skill" and dispatches via handleChat(skillPrompt, { silentUser: true }). Critically, silentUser only skips rendering the user message — at line 340 it still calls sessionState.addExchange({ role: "user", content: text }), injecting the synthesized prompt into the context window. A true silent skill invocation must NOT add a user exchange.
  • src/tui/conversationArea.js:591-700 — createStreamingHandler shows the exact event shapes the web client must mirror: message, reasoning, on_tool_start, on_tool_end, tool_result, on_tool_error, on_chat_model_stream. The frontend reimplements this rendering (message bubbles, reasoning side-channel, tool call display) in the browser.
  • src/tui/app.js:60 — the TUI reads the token budget ceiling from config.providers[providerName].rateLimit.maxTokensMinute. The SPA's token indicator needs the same ceiling plus live usage.
  • src/provider/tokenBudget.js — createTokenBudget exposes current() (rolling usage) and reserve()/reconcile()/release(). The GET /api/tokens endpoint reads budget.current() for live usage. The shared instance is getSharedTokenBudget(maxTokensMinute) in src/provider/openai.js:25.
  • src/tui/settingsPanel.js — read-only config browser; flattens nested config into { path, value } pairs. The SPA Settings view mirrors this.
  • src/tui/skillsPanel.js — DESCRIPTION_MAX_LENGTH = 500; lists skills with name + description. The SPA Skills view mirrors this.
  • src/tui/sessionsPanel.js — lists memory/sessions/*.md, synthesizes a topic from the first user message. The SPA Sessions view mirrors this.
  • src/tui/memoryPanel.js — browses memory/context/, filtering out ephemeral-*.md, reflection.md, clarifications.md. The SPA Memory view mirrors this.
  • src/tui/projectsPanel.js — lists directories under projects/, excluding *.worktrees. The SPA Projects view mirrors this.
  • src/skills/registry.js — SkillRegistry exposes list(), get(name), getCatalog(), getSkillBody(name), getSkillPaths(). The silent skill-execution endpoint can resolve a skill by name and dispatch it without a context-window message.
  • No HTTP server exists — grep for express|fastify|hono|createServer|ws|socket.io|SSE across src/ and index.js returns nothing. The container runs SSH + cron + the Ink TUI. The web layer is greenfield but sits on a stable engine.
  • No web/ directory exists — the frontend is greenfield. The Dockerfile copies src/, prompts/, .skills/ but has no frontend build step or static asset copy. The Vite source lives in web/, builds to web/dist/, and is served by @fastify/static.
  • config.yaml — no web section today; needs web.host (default 127.0.0.1), web.port, and web.staticDir.

Fix Steps

  1. Add config schema — Create src/config/schemas/web.js with host (default 127.0.0.1), port (default e.g. 3000), staticDir. Register it in src/config/schemas/index.js and add a web: section to config.yaml.
  2. Extract shared command dispatch — Refactor CommandParser (or extract a shared command module) so both the TUI and web layer dispatch commands identically. This is the single source of truth for the 1:1 API surface.
  3. Create the Fastify server — Add src/web/server.js exporting a createWebServer({ config, sessionState, dispatchProvider, registry, scheduleManager }) that registers the session-scoped endpoints (see API Surface): POST /api/sessions, GET /api/sessions, GET /api/sessions/:id, POST /api/sessions/:id/messages, POST /api/sessions/:id/interrupt, plus the global commands: config (GET/PATCH), gc, help, memories, provider, projects, projects/select, projects/clear, quit, schedule, settings, skills, skills/:name/execute, and tokens.
  4. Implement SSE streaming — In POST /api/sessions/:id/messages, load the session's conversation into the session state, call dispatchProvider(message, sessionState, streamingCallback, signal) and write each streaming chunk as an SSE data: event. Map message/reasoning/on_tool_start/on_tool_end directly. Persist the exchange via sessionState.addExchange + saveSession after the stream completes. Track in-flight streams per session so POST /api/sessions/:id/interrupt routes to the correct AbortController.
  5. Implement silent skill execution — Add POST /api/skills/:name/execute that resolves the skill via SkillRegistry, dispatches it WITHOUT calling sessionState.addExchange({ role: "user", ... }). This requires a new dispatch path (or a silent flag on the dispatch that skips the context-window injection, distinct from the TUI's silentUser which only skips rendering).
  6. Add the web mode branch — In index.js, add web to the mode detection and start the server (reusing the already-built agent, sessionState, dispatchProvider) instead of rendering the TUI.
  7. Build the frontend — Scaffold web/ with Vite + Tailwind. Implement the six views (Conversation, Skills, Memory, Settings, Sessions, Projects) and the status bar per the SPA Specification. Use the defined data contracts for each panel endpoint.
  8. Wire the Docker container — Add a frontend build step (Vite build → web/dist/), copy the built assets into the image, expose the web port, adjust docker-entrypoint.sh/CMD so the container can run --mode web, and document the port mapping.
  9. Write tests — Add tests/unit/web/server.test.js (route validation, SSE framing, interrupt semantics, silent skill execution) and tests/integration/web/server.test.js (POST chat → assert SSE events → assert session persisted; POST skill → assert no user message added to context). Run npm run test, npm run lint, npm run coverage.
Lingua principale
JavaScript
Stelle
2
Fork
0
Merge medio
1h 17m
PR unite (30g)
242

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di avoidwork/madz

Tutte le issue di avoidwork/madz

Issue simili

Altre issue su JavaScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.