feat: add web interface served from docker container
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
- Ambito
- api, full-stack, testing, web-dev
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
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
dispatchProviderstreaming 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:
- Client calls
POST /api/sessions→ gets{ id }. - Client calls
POST /api/sessions/:id/messageswith{ content }→ server loads that session's conversation into the session state, dispatches viadispatchProvider, and streams SSE events (message,reasoning,on_tool_start,on_tool_end). The exchange is appended to the session and persisted. - To continue, client calls the same
POST /api/sessions/:id/messagesagain — the session ID carries the context. - 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 forreasoningsegments, rendered offset so it does not fragment the message flow. - Tool call display —
on_tool_start/on_tool_end/tool_result/on_tool_errorevents rendered as inline tool-call chips (name + status), matchingcreateStreamingHandlerinsrc/tui/conversationArea.js:591-700. - Markdown rendering — assistant content rendered as markdown (the TUI uses
markedviasrc/tui/markdownText.js). - Slash command input —
/triggers command autocomplete; unknown/skillNameroutes toPOST /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_LENGTHinsrc/tui/skillsPanel.js). - Select a skill → invoke via
POST /api/skills/:name/execute. - Data source:
GET /api/skills→{ skills: [{ name, description, location }] }(fromregistry.getCatalog()).
View 3 — Memory panel
- Browse canonical user memories from
memory/context/, excludingephemeral-*.md,reflection.md,clarifications.md(matchingsrc/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 (matchingsrc/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(matchingsrc/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 sharedTokenBudget+ the configured ceiling). The TUI reads the ceiling fromconfig.providers[].rateLimit.maxTokensMinute(src/tui/app.js:60) and the live usage frombudget.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 emitsmessage/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. Usenode:testwithassertto 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/skillNameroutes 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/tokensusage. - 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.1by default (no auth, per requirements). Document that exposing the port to the network requires adding auth — the container runs asmadzwith 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 /interruptcovers 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/sessiontrio 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:
- Run
/opsx:proposeto generate a full proposal with specs and tasks - Iterate on the design before any code is written
- 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 callscallProvider(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— exportsconfig,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 awebmode here that reuses the already-initializedagent,sessionState, anddispatchProviderinstead 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— exportscreateSession,SessionStateManager,loadSession,saveSession,createCheckpointer,handleShutdown. Session persistence is already on disk and resumable; the web layer reuses it unchanged.src/config/loader.js— exportsloadConfig,setConfigValue,saveConfig. A newwebconfig section (host, port, static dir) needs a schema insrc/config/schemas/and aconfig.yamlentry.src/tui/commandHelp.js—COMMAND_GROUPSis 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—CommandParserdispatches 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/skillpath synthesizes"Run the <skill> skill"and dispatches viahandleChat(skillPrompt, { silentUser: true }). Critically,silentUseronly skips rendering the user message — at line 340 it still callssessionState.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—createStreamingHandlershows 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 fromconfig.providers[providerName].rateLimit.maxTokensMinute. The SPA's token indicator needs the same ceiling plus live usage.src/provider/tokenBudget.js—createTokenBudgetexposescurrent()(rolling usage) andreserve()/reconcile()/release(). TheGET /api/tokensendpoint readsbudget.current()for live usage. The shared instance isgetSharedTokenBudget(maxTokensMinute)insrc/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— listsmemory/sessions/*.md, synthesizes a topic from the first user message. The SPA Sessions view mirrors this.src/tui/memoryPanel.js— browsesmemory/context/, filtering outephemeral-*.md,reflection.md,clarifications.md. The SPA Memory view mirrors this.src/tui/projectsPanel.js— lists directories underprojects/, excluding*.worktrees. The SPA Projects view mirrors this.src/skills/registry.js—SkillRegistryexposeslist(),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|SSEacrosssrc/andindex.jsreturns 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 copiessrc/,prompts/,.skills/but has no frontend build step or static asset copy. The Vite source lives inweb/, builds toweb/dist/, and is served by@fastify/static. config.yaml— nowebsection today; needsweb.host(default127.0.0.1),web.port, andweb.staticDir.
Fix Steps
- Add config schema — Create
src/config/schemas/web.jswithhost(default127.0.0.1),port(default e.g.3000),staticDir. Register it insrc/config/schemas/index.jsand add aweb:section toconfig.yaml. - 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. - Create the Fastify server — Add
src/web/server.jsexporting acreateWebServer({ 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. - Implement SSE streaming — In
POST /api/sessions/:id/messages, load the session's conversation into the session state, calldispatchProvider(message, sessionState, streamingCallback, signal)and write each streaming chunk as an SSEdata:event. Mapmessage/reasoning/on_tool_start/on_tool_enddirectly. Persist the exchange viasessionState.addExchange+saveSessionafter the stream completes. Track in-flight streams per session soPOST /api/sessions/:id/interruptroutes to the correct AbortController. - Implement silent skill execution — Add
POST /api/skills/:name/executethat resolves the skill viaSkillRegistry, dispatches it WITHOUT callingsessionState.addExchange({ role: "user", ... }). This requires a new dispatch path (or asilentflag on the dispatch that skips the context-window injection, distinct from the TUI'ssilentUserwhich only skips rendering). - Add the
webmode branch — Inindex.js, addwebto the mode detection and start the server (reusing the already-builtagent,sessionState,dispatchProvider) instead of rendering the TUI. - 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. - Wire the Docker container — Add a frontend build step (Vite build →
web/dist/), copy the built assets into the image, expose the web port, adjustdocker-entrypoint.sh/CMD so the container can run--mode web, and document the port mapping. - Write tests — Add
tests/unit/web/server.test.js(route validation, SSE framing, interrupt semantics, silent skill execution) andtests/integration/web/server.test.js(POST chat → assert SSE events → assert session persisted; POST skill → assert no user message added to context). Runnpm 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
- Include un Dockerfile o un file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di avoidwork/madz
-
feat: add config-driven MCP server tool registrationForse già presa @avoidwork l’ha presa oggi. Apertaapproved feature in progress
Difficoltà 5/5 Più di una settimana Idoneità per principianti 12/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di avoidwork/madz
Issue simili
-
Remove: Fox Deportes SDApertacheck:passed feeds:remove
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 65/100
iptv-org/database#37176 · 1 commento · 1 reazione ·
I maintainer di solito rispondono entro 9 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
hawk-digital-environments/HAWKI#443 ·
I maintainer di solito rispondono entro 1 giorno
-
documentation v2
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
modelcontextprotocol/python-sdk#3662 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno
-
feedback simulation workshop
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
githubnext/gh-aw-workshop#4370 ·
I maintainer di solito rispondono entro 1 giorno