We are building a web UI and web-renderer system for AppWeaver. Current goal
- Support rich web renderers for commands/subcommands, starting with a custom web renderer for
todo list. - Keep the existing text command flow working.
- Reuse existing command handlers/business logic where possible.
- Allow web UI actions to map into normal command invocations first, instead of overengineering adapters. What already exists
Reusable local date/time display: timestamp elements accept timestampMs
(epoch milliseconds) and an optional label prefix. The client formats them in
the browser's locale/timezone, including the timezone abbreviation, with an ISO
datetime/tooltip for the exact instant. Standard text size/tone/style props apply.
Backend/web
- Local web server exists in
src/web/server.ts,src/web/routes.ts,src/web/command-catalog.ts,src/web/execute.ts,src/web/chat.ts. - Web API supports:
GET /api/healthGET /api/commandsGET /api/commands/:namePOST /api/commands/:command/:subcommandPOST /api/chat
- Plugin commands are included in web command discovery if the plugin exposes
commandDefinition. - Plugin commandDefinition can now be either:
- a concrete
CommandDefinition - or a function
(prefix, alias) => CommandDefinition
- a concrete
src/commands/help/handlers.tswas updated to resolve plugin command definitions correctly. Frontend/web- Solid + Vite app exists under
web/ - Main files:
web/src/App.tsxweb/src/components/TimelineView.tsxweb/src/components/CommandPalette.tsxweb/src/components/Composer.tsxweb/src/types.tsweb/src/utils.ts
- Current UI supports:
- timeline/chat shell
- bottom composer
/command popup- command and subcommand filtering
- auto-focus behaviors
- command execution through HTTP
- chat through
/api/chat
- The popup path parser supports command/subcommand-ish input such as
todo list, and special treatment for/help. Command helper work already done src/system/command-helpers.tsnow exists with:createCommand(...)createSubcommand(...)
- It uses the shared representation base from:
src/system/representation.ts
- Helper-based subcommands currently include:
definitionrepresentationhandlerrenderers- optional
adapter
- Important: adapters are optional by design. Do not force adapters everywhere.
- The current practical rule is:
- prefer transforming web actions into normal command invocations first
- only add adapters if a command truly needs one later Important architectural decisions already made
- We want renderer targets to be:
textwebnotcli
- We are leaning toward adding
webtoMessageSourceand eventually removingpluginfromsrc/messaging.ts, but that has NOT been fully implemented yet. - Long-term renderer selection should likely be global/shared, not per-plugin hacked in adapters.
- But we have NOT yet implemented a full target-aware renderer dispatcher.
- For now, web still mostly executes commands through existing command-text paths or reconstructed command payloads. What we want next Primary next goal
- Build a real custom web renderer for
todo list. What “todo list web renderer” should mean - A dedicated web view for the
todo listsubcommand, not just plain text in a card. - It should render the todo tree/list in structured HTML/UI.
- Desired interactions we discussed:
- click checkbox to mark item
in_progress - double click to mark item
done - click
[x]to delete - edit item text
- insert new item
- move/reorder item, maybe change parent
- maybe save-all mode, maybe immediate execution mode
- click checkbox to mark item
- However, we decided NOT to overbuild a generic action-adapter system yet.
- First preference:
- map web UI actions back into normal command invocations where possible, such as:
/todo update .../todo done .../todo delete .../todo add .../todo move ...
- after each action, refresh the rendered list by re-running the list command or equivalent
- map web UI actions back into normal command invocations where possible, such as:
- Only add custom web-only action plumbing if command-shaped mapping becomes clearly awkward. What is still missing
- No real renderer selection system yet (
textvsweb). - No standard place yet where command handlers return
representation + renderersand dispatch chooses target. - No actual custom web renderer has been wired into command dispatch yet.
todo listcurrently still goes through the generic command form/result system.MessageSource.weband target-aware rendering are still conceptual, not finished infra.- No websocket live event system yet; current web messaging uses HTTP.
Practical next step
Implement the smallest clean vertical slice for
todo listcustom web rendering, without overhauling the whole architecture. Recommended scope for the next step
- Inspect the todo plugin structure, especially:
plugins/todo/commands/list/*- existing handler/representation/text rendering
- Add a custom
renderers/web.tsfortodo list - Keep the current web shell architecture, but teach it to recognize/render the
todo listresult specially - Reuse existing command execution for actions when possible
- for example, a click in the web renderer can call the existing
/api/commands/...endpoint with the right command/subcommand/payload
- for example, a click in the web renderer can call the existing
- After an action succeeds, refresh the
todo listweb view by re-running list with the same filters/options - Do not try to solve every renderer/dispatcher abstraction globally unless needed for this slice
- Prefer one focused working example over a generalized framework that is not yet exercised Suggested approach
- Start by inspecting:
plugins/todo/commands/list/handler.tsplugins/todo/commands/list/representation.tsplugins/todo/commands/list/renderers/text.tsor equivalent existing text renderer
- If there is no suitable representation for web, extend or improve the
todo listrepresentation so it contains enough structured data for a web view. - Then create a web renderer for that representation.
- Wire the frontend so when a
todo listresult is returned, it renders a structured todo view instead of plain text. - Keep fallback to plain text if web rendering is unavailable. Important constraints
- Do not force adapters everywhere.
- Do not require every command to have a web renderer yet.
- Do not break existing text/CLI behavior.
- Keep the new helper system optional.
- Prefer incremental integration over large-scale refactors. Useful context on code quality
src/web/*backend code is currently cleaner and more structured than the frontend was.web/src/App.tsxwas recently split into components and is now more manageable.- The next good cleanup after the todo renderer may be:
- further splitting timeline card components
- introducing a global renderer-selection helper
- But do not do those first unless needed to make
todo listwork well. Deliverable for this session - A working first custom web renderer path for
todo list - Enough backend/frontend wiring so the web UI can show a structured todo list and trigger at least one or two meaningful actions through the existing command execution path
- A short explanation of the architecture chosen and what should come next after this slice My practical recommendation:
- yes, start the new session with exactly this
- the next logical step is not a full renderer framework
- it is one real todo list web renderer slice that proves the model
Scoped styles (Shadow DOM)
Command web UIs (WebNodeRoot, kind: 'ui') render inside a Shadow DOM island on the web client. Shared primitives (.web-node, .web-button, tree, overflow menu, tone utilities, etc.) come from a base stylesheet injected automatically; optional stylesheets on the root payload add presentation-only CSS scoped to that render.
Plugin / handler guardrails:
stylesheetsare presentation-only. Do not rely on global app classes fromweb/src/styles.css; only:roottheme variables (for examplevar(--color-accent)) are guaranteed to inherit into the shadow tree.- Prefer stable string
idvalues per logical sheet (for examplefile-plugin-tree); the client dedupes byidand replacescssTextwhen the sameidappears again on an updated root. - Avoid
@importand remote fonts in v1 payloads; keep CSS self-contained and small.
Schema: WebStyleSheet in src/web/ui-schema.ts ({ id, cssText }); optional stylesheets array on WebRenderResultSchema / WebNodeRoot.
Opt-in live refresh
WebNodeRoot.autoRefreshMs (2,000–60,000 ms) opts a render into polling its own meta command. Use it only on read-only view commands, never a command that starts execution. The Shadow DOM host skips polling while hidden/unmounted, busy, or while an input/textarea/select/contenteditable is focused. Requests do not overlap, create timeline entries, or show a pending overlay. Responses use normal root reconciliation and stale-response handling; polling is disposed when the render changes or unmounts. Include a manual Refresh action for recovery after connection errors.
Shadow mount overflow: optional shadowMountOverflow on WebNodeRoot ('hidden' | 'scroll-y'). Omitted or 'scroll-y' lets the inner Solid mount scroll when content is taller than the host (typical timeline cards). Use 'hidden' when your tree defines its own scroll regions (for example a fixed chrome row plus an overflow: auto panel inside stylesheets). The file tree web renderer sets 'hidden' so only .web-file-tree-block scrolls in the modal.
Stable identity and pending UI
Refreshed WebNodeRoot payloads are authoritative complete trees. The client reconciles compatible element nodes so stateful components can keep local UI state while server-rendered props, children, metadata, and stylesheets update.
- Put
renderKeyon repeated or stateful element nodes. It must be stable across equivalent refreshes, unique among siblings, independent of sorting/counts, and never reused for another element tag. - Include branch context when one entity appears in several places, for example
topic:nostr:event:<id>andmood:focused:event:<id>.renderKeyidentifies one rendered instance. - Put
entityKeyin element props when several render instances represent one logical entity, for examplenostr-event:<id>. Duplicate entity keys are intentional and are not reconciliation keys. - Unkeyed legacy nodes continue to reconcile positionally. Reordered stateful collections should always provide keys.
- The client uses a focused recursive WebNode reconciler rather than Solid's stock keyed array reconciler because WebNode arrays may mix keyed elements, unkeyed elements, and text nodes.
Command actions may choose pending presentation:
An explicit surface: 'timeline' action renders its result in the main timeline, even when the target command has widget/dock metadata. It does not replace the origin widget or modal root. Actions without a surface can update their origin in place via onReplaceRoot.
pendingUi: {
presentation: 'entity',
label: 'Updating...',
}
- Omitted or
widgetkeeps the widget-wide Working overlay. entityuses the nearest ancestorentityKey, marks every mounted copy in the same widget source pending, and falls back to widget pending when no entity is available.noneruns without a pending overlay.- Keep the action's refresh command server-authoritative. Entity pending remains active through refresh settlement, and late older refresh roots are ignored.