WebChat - Transform CLI into a Conversation
WebChat makes any agent CLI available through a chat-style interface that users can reach from their browser.
What WebChat Delivers
Conversational UI
Stream CLI input/output through a WhatsApp-inspired interface with bubbles, timestamps, and typing indicators.
Router Login
WebChat uses the router login flow, so access follows the same local or SSO session model as other authenticated router surfaces.
Agent Ready
When a static workspace agent is configured, WebChat automatically connects to that agentβs manifest cli command.
Access
Use ploinky webchat to print the current access URL. The command reports the router login URL; --rotate is accepted for compatibility but WebChat does not use a surface-specific token.
ploinky webchat # show the router login URL
ploinky webchat --rotate # show the same login URL
http://127.0.0.1:8080/webchat
How Commands Are Chosen
WebChat derives its runtime command from the static workspace agent:
- Static agent β Configure one with
ploinky start <agent> <port>. The router records the agentβs manifest path. - Manifest CLI β When the agentβs
manifest.jsondefines aclicommand, WebChat runs it inside the agent container. - No CLI? β Sessions still open, but the chat will display a notice that no command is bound.
This flow depends entirely on the static workspace agent: keep its manifest updated to control which CLI the chat session runs.
URL Parameters and CLI Arguments
WebChat accepts arbitrary query parameters in its URL. When an explicit agent query parameter is present, the router keeps the remaining query parameters attached to the WebChat session and forwards them to that agent's CLI command as long-form CLI flags encoded as single tokens.
/webchat?agent=achilles-cli&path=/absolute/path
# launches the selected agent as:
ploinky cli achilles-cli --path=/absolute/path
The router reserves agent for agent selection and tabId for the browser client. These internal values are not forwarded as agent CLI flags. Agent CLIs remain responsible for interpreting ordinary forwarded flags and values.
Session Experience
Agent-Owned Sessions
WebChat does not store conversation history. A compatible selected CLI owns its durable sessions and publishes validated __webchatSession snapshots for browser rendering. Refreshing the page reuses the surviving agent process and its latest in-memory snapshot, while the browser keeps its client-only tabId in sessionStorage.
Runtime reuse is health-aware. WebChat checks that the selected TTY child is alive and its stdin is writable before accepting a prompt. A failed write returns 409, removes the stale runtime, and is not broadcast to other subscribers as accepted; the client retries once after reconnect and otherwise stops the thinking indicator with an input error. Healthy runtimes remain shared across browser refreshes.
The selected CLI may store each submitted natural-language turn as the user message followed immediately by an assistant placeholder. Commands submitted visibly through the composer may also be stored for presentation, while commands sent silently by Sessions, Skills, Tasks, and task-view controls remain outside the transcript.
The placeholder starts with empty final text and an optional progress array of strings. Task references are separate conversation items rather than properties of the assistant message.
WebChat renders progress live without persisting it; the CLI may include progress reasons and final output in its later session snapshot.
WebChat removes transport echoes such as its structured input envelope before displaying output. Ordinary assistant text remains valid when it happens to equal the submitted user message.
- Click to load session history appears as a centered standalone button inside the scrollable conversation, rather than as a chat message, and renders the current session only when requested. It is browser-only, is never stored as a message or sent to the agent, and disappears immediately when activated; the composer remains available while older messages are hidden.
- Sessions sends
/sessionto the selected CLI. Its first, emphasized New item sends/session new. - Skills sends
/skills, shows total and enabled counts, and displays every registered workspace skill, including disabled entries, in a path tree that is fully expanded by default. Directory-only chains are compacted, soexample/path/skill-nameappears as folder/example/pathand leafskill-name. Each folder and terminalname (type)entry has a checkbox immediately before its name. Folder checkboxes are tri-state and modify all descendants in a local draft; no enable/disable command is sent until Save. Saving compacts the draft into plural/skills enable|disable <relative-directory>commands plus singular/skill enable|disable <canonical-name>exceptions, applies them in order, reloads the authoritative list, and refreshes slash autocomplete so disabled skills disappear from/exec. - Existing session items show the first user-message preview and relative activity time, such as
2 days ago, without message counts or agent metadata. - At normal desktop widths, the header retains its visible Tasks, Skills, Sessions, and Logout controls, with the working directory centered inside the header and bounded by an ellipsis. At 640 pixels or less, those same controls move into the three-dot menu and the working directory joins the second metadata row so the selected agent, current model, connection state, and working directory remain visible.
- Selecting an existing item sends
/session resume <session-id>. The slash autocomplete can display the session name supplied by the CLI while inserting its opaque id after/session resume. Connected browser clients receive the CLI's session-state changes through the shared EventSource runtime.
If the original process no longer exists, the selected CLI loads its own current session and supplies prior natural-language turns once when it creates its agent session. Ploinky does not replay historical commands or append history to the WebChat input envelope. Each outgoing envelope only distinguishes visible composer input from silent UI control input; the CLI owns persistence. Slash commands do not consume the CLI's pending one-time hydration, and visible command records are excluded from model context even when restored for display.
Ploinky does not set a conversation-history environment flag or forward a selected conversation id. The CLI determines whether its own current session is empty.
Interactive Requests
A compatible CLI can pause an active request and publish a generic structured interaction with bounded choices. WebChat displays those choices in a dedicated selector above the composer, selects the CLI-declared default, and supports Arrow Up/Down, Enter, or pointer selection. Ordinary message submission remains disabled until the interaction is resolved.
The decision travels through an authenticated control route and is matched to the active tab, runtime, request id, and one of the declared options. It is not a chat message and is restored as a selector after an EventSource reconnect while the CLI still has it pending.
Runtime Model Status
A compatible CLI may publish a generic __webchatRuntimeState line envelope containing its currently selected model. WebChat removes that control line from assistant output and history, keeps the latest value only in the live runtime, and shows a compact model badge beside the agent name. The runtime-state snapshot is sent again after an EventSource reconnect. A null model hides the badge.
The badge describes agent-declared configuration, not the effective provider model used for an individual answer. WebChat does not read agent settings files, persist the badge in folder history, or attach model metadata to messages.
Conversation restoration does not use a process-instance identifier. The selected CLI owns process startup, current-session loading, and one-time agent hydration; WebChat only retains the latest session snapshot for reconnecting browsers.
Workspace File Index
A compatible CLI can publish an initial workspace-file snapshot and later added/removed deltas for its current working directory. WebChat removes these control records from assistant output, keeps the index only in live runtime and browser memory, and sends a complete snapshot again after an EventSource reconnect.
Recognizable assistant paths become preview links only when their normalized cwd-relative target
is present in that index. New files can enhance messages already on screen, and deleted files turn
previously inferred links back into text. Opening a link still goes through the authenticated
/workspace-files/... route, which independently validates the path and remains the
final read authority.
Background Tasks
CLIs may publish generic asynchronous task list, view, lifecycle, log, and action envelopes through their stdout stream. The selected CLI owns task metadata, logs, reattachment, and actions in its workspace store. WebChat validates these envelopes, retains only volatile runtime state, and presents it through the authenticated Tasks overlay. A browser reconnect receives the surviving runtime's revalidated task-list snapshot before later live updates; logs remain in the selected CLI. The Tasks button sends /tasks; selecting a task sends /task view <task-id>. Ploinky does not write task state or logs and exposes no task data REST API.
Every started task appears immediately as its own compact incoming-style chat item. One assistant turn may create multiple task items, kept after its assistant message in task-start order even when final assistant text arrives later. For a visible command such as /exec launch-opencode hi, the selected CLI persists the command, its response, and each task reference in the same ordered session snapshot, so refresh restores all three. Silent /tasks, /task view, stop/continue, and session-control requests remain UI actions rather than transcript turns; their textual acknowledgements and errors are also suppressed from the main chat. The first row shows the exact agent id, description, current status, and elapsed seconds; the second row provides a View task details link instead of inline expansion. The link opens the authenticated task HTML page in WebChat's right-side panel. In that panel the page asks its parent to send /task view for initial state and recovery, then receives matching updates from the parent's existing EventSource through a same-origin message bridge. When opened directly in a separate tab, the page preserves the workspace and selected-agent query, attaches to the same generic WebChat runtime with its own tab id, requests the task immediately, and repeats the request after reconnect. Temporary runtime-startup 409 responses receive bounded retries, and the page shows a loading state until data or a terminal transport error arrives. A mismatched log offset triggers one full snapshot request and waits for the real response instead of recursively replaying the same delta. Neither display mode fetches task data through a task-specific REST endpoint. The item survives completion and history reload. Old assistant messages carrying a taskId property are ignored instead of migrated. Pending tasks show QUEUED, active tasks show RUNNING, and terminal tasks show COMPLETED, STOPPED, or FAILED.
The right-side panel is generic for task pages and all other links opened from chat. It ends above the floating composer, follows the composer's dynamic height, and scrolls its own content or embedded page in the remaining space, so no part of the opened page is hidden underneath the input pill. On mobile it becomes a full-width panel anchored below the primary WebChat header, keeping its link title and 44-pixel close action visible above the embedded page.
The selected CLI keeps provider task output in its task log. When completion supplies the final MCP result separately, CLI ingestion finds the last identical range already present in that log and stores only its offset and length; it never appends or duplicates result text. The task journal retains one final-output range for every completed continuation turn. The Tasks overlay and task page show lines outside those ranges in lighter muted grey and every retained final-answer range in bold primary text, including earlier answers while a continuation is running. A live range-only update rerenders the existing log immediately. Every transition to a terminal state also requests one authoritative task snapshot, recovering final classification or a last log delta that was unavailable in the live event without requiring another provider poll or browser refresh. Legacy tasks that expose only one final range remain compatible. Continuation logs contain the accented you> <prompt> line without a synthetic turn label; the browser also suppresses historical [Continuation N] labels and normalizes older User: prompt lines. Task-log regions reserve a visible, theme-aware scrollbar so long histories can be navigated directly. The renderer shows generic paths in green without bold weight and gives backtick-delimited fragments the accent color used for file-link text, without an underline or other link decoration. Both remain non-interactive spans: they preserve the displayed text and stored log exactly and never become links. The browser strips ANSI control sequences and retains display compatibility for historical prefixed logs; new raw provider output is otherwise unchanged.
When a task finishes, stops, or fails, WebChat shows a transient notification in the upper-right corner. Its right-side close button dismisses it immediately; automatic expiry remains available when no action is taken.
Closing the browser tab does not stop an ongoing delegated task. WebChat retains the selected CLI runtime while its volatile snapshot contains ongoing work so CLI-owned polling and log capture continue. The runtime returns to the normal reconnect cleanup policy after its tasks reach finished, stopped, or error. A recreated compatible CLI reattaches from its own task journal by target agent and remote task id.
The task page shows a direct Stop action for queued and running tasks. It sends /task stop <task-id> to the selected CLI, which resolves the stored target and remote id and makes a request-bound, router-mediated AgentServer cancellation call. Queued work is removed before execution. Running work enters STOPPING, receives graceful termination, and is force-terminated after a two-second cleanup window if necessary. No browser credential is forwarded to the target agent, and the action does not start a stopped provider merely to cancel it.
A completed or failed task, and a cancelled running task whose provider session was saved before termination, may advertise a generic continuation capability. A queued task cancelled before execution has no session and cannot be continued. When the capability exists, the task page sends /task continue <task-id> <prompt> to the selected CLI, which resolves the stored target agent, tool name, and opaque handle. Enter sends the message; Ctrl/Cmd+Enter or Shift+Enter inserts a newline. The field cannot be resized manually: it grows upward with its content to the same bounded height as the main composer, then becomes scrollable, and shrinks again when content is removed. Every message starts a new remote AgentServer execution, but the CLI-owned local taskId and original title remain unchanged; only the task's turn increases. The CLI first appends the submitted prompt to the same full-retention log and publishes that delta, then appends provider output in arrival order. If the stored provider is a stopped startup: manual agent, the selected CLI activates that exact provider in global mode through Ploinky's internal lifecycle and waits for readiness before invoking it. Continuation uses internal MCP policy for the verified CLI-to-provider call plus request-bound router authorization; the browser never invokes the provider tool directly. Startup failures remain explicit and the provider is never replaced silently.
Message Rendering
- β Automatic timestamps for each exchange
- β Rich markdown rendering (tables, code blocks, lists)
- β Long output folding with βShow moreβ expansion
- β Agent progress reasons persisted as an ordered string array and displayed as a collapsible block above the final answer
- β Side panel to inspect full message history
- β
Workspace file previews β recognizable cwd-relative paths in assistant output become links
only when the selected CLI's live workspace index contains the file, without changing the stored message. Markdown is rendered with the WebChat Markdown renderer,
text and source files use an escaped code view, images and PDFs use native previews, and HTML
opens in a sandboxed frame. All content is read through the existing authenticated
/workspace-files/...route. - β Workspace-wide Tasks overlay with live bounded logs and completion notifications
- β Workspace Skills explorer with expandable directory branches, tri-state checkboxes, staged changes, and explicit Save
Input Controls
- β Auto-resizing composer with keyboard shortcuts (Enter to send, Shift+Enter for new line)
- β Light/Dark/Explorer/Obsidian Dark theme selector with persistent preference (Explorer loads by default; switch via the three-dot menu β Appearance)
- β
Slash command autocomplete β WebChat loads the selected agent's catalog during page startup and retries transient startup failures with bounded backoff for approximately 30 seconds. Concurrent initial refreshes share one request sequence. Start the composer with
/to open the fixed-height menu from the loaded catalog; typing/does not make another MCP request. Later slashes remain argument text, Arrow Up/Down keeps the active item visible, and large agent-declared result sets remain fully accessible through progressively rendered scroll batches. - β
Composer
@path autocomplete β typing@opens the files and folders in the working directory selected by the WebChatdirlaunch parameter, and every additional path character refreshes the filtered suggestions. Selecting a folder drills into it; typing a space after the folder ends autocomplete so the folder can be referenced directly. Selecting a file inserts a cwd-relative token such as@src/index.jswith a structured workspace reference and closes the menu, so the next Enter sends the message. - β Cancel button β appears during active processing; sends an interrupt signal to the agent
Workspace Uploads
After choosing a file, folder, or camera capture, WebChat opens its own destination explorer at the current working directory. Browse with the breadcrumb, create a folder when needed, and choose Upload here. The selection appears above the composer immediately, while its bytes are uploaded only when the message is sent.
- The explorer lists the immediate contents of the selected directory. Folders can be opened; files are visible for context but cannot be selected as destinations.
- Folder uploads preserve
file.webkitRelativePath, including the selected folder name. Existing folders are merged after confirmation: colliding files are replaced and unrelated files remain. - An existing file is never replaced without an explicit warning and confirmation. File-versus-folder conflicts remain blocked.
POST /webchat/uploadswrites directly below the selected working-directory path and returns an authenticated/workspace-files/...link. WebChat does not create anuploadsfolder, hashed storage path, persistent upload id, or MIME metadata file.- The server rejects absolute paths, traversal segments, NUL bytes, symlink paths, Ploinky runtime state, dependency directories, and reserved
.secrets/*.secretsnames.
Security Practices
- Use HTTPS or SSH tunnels when exposing the WebChat URL beyond localhost.
- Protect router session cookies and the encrypted
.ploinky/.secretsstore used by local auth, dashboard tokens, and manifest env. - Conversation and task persistence belong to the selected CLI. WebChat retains validated session and task snapshots only in runtime memory.
Troubleshooting
- Blank session? Verify the static agent has a
clicommand in its manifest. - 401 errors? Run
ploinky webchatagain to confirm the login URL, then authenticate in the router session. - Need a different command? Update the agent manifest and restart the workspace so WebChat picks up the change.
- Custom launch flags ignored? Confirm you opened
/webchatwith?agent=<name>; without an explicit agent selection the router uses the default WebChat target and does not synthesize agent-specific CLI flags. - Message does not start? A
409response means the selected CLI process ended or its stdin became unavailable. WebChat disposes that runtime automatically; allow the stream to reconnect before sending again.