126 lines
8.3 KiB
Markdown
Executable File
126 lines
8.3 KiB
Markdown
Executable File
# ADR-003: Workspace Tab (Peek-Style Orchestration)
|
||
|
||
**Date:** 2026-07-07
|
||
**Status:** Accepted
|
||
|
||
## Context
|
||
|
||
The v3.0 workflow for interacting with a running tool required context-switching out of the AI-LSC app:
|
||
|
||
- Web-interface tools (OpenWebUI, Hermes, Odysseus, ComfyUI, etc.) required opening a browser tab and navigating to `http://127.0.0.1:{port}`.
|
||
- CLI tools (Aider, Claude Code, OpenHands, etc.) required opening a terminal and `tmux attach -t ai_lsc_<uid>::<tool_id>`.
|
||
|
||
When debugging a stack of 8+ tools, this meant 8+ browser tabs + 8+ terminal windows — exactly the juggling the app was supposed to eliminate.
|
||
|
||
The user asked for a "peek orchestration" surface that "feels like managing VMs from virt-manager or aqemu" — every staged tool reachable from a single window, no context-switch to a browser or terminal app required. The user also raised the option of using servo (Mozilla's Rust web engine) to embed web tools directly, calling it a "workspace" that "can have multiple tabs for various tools like openwebui, hermes dashboard, or odysseus even."
|
||
|
||
## Decision
|
||
|
||
Add a new `WorkspaceTab` widget with one sub-tab per active tool. The sub-tab type is chosen by the tool's feature flags:
|
||
|
||
- `has_web=True` → `_WebToolPage` — embeds the tool's web UI via `QWebEngineView`
|
||
- `has_cli=True` (and no web) → `_CliToolPage` — embeds the tool's tmux session via `tmux capture-pane` polling
|
||
- otherwise → `_PlaceholderPage` — explains the tool is passive / library
|
||
|
||
### Web embedding: QWebEngineView (with servo-swap path documented)
|
||
|
||
The web embedding uses `PySide6.QtWebEngineWidgets.QWebEngineView` by default. Servo's Python bindings are not yet production-ready and have no first-class PySide6 integration, so servo is not the default. However, the module is structured so swapping in servo later is a one-line change: replace `_make_web_view()` with a servo-backed widget that exposes the same `setUrl()` / `url()` / `load()` API. The rest of `_WebToolPage` only depends on that API.
|
||
|
||
The `_WebToolPage` includes:
|
||
|
||
- A URL bar at the top showing the loaded URL (monospace, slate-50 background)
|
||
- A ⟳ reload button
|
||
- A ↗ "open in external browser" button (fallback for when QtWebEngine is not installed)
|
||
- The `QWebEngineView` itself, stretched to fill the remaining space
|
||
|
||
When `QtWebEngine` is not installed (some minimal PySide6 installs skip it), the page falls back to a `_PlaceholderPage` explaining the situation and offering the ↗ button to open the URL externally.
|
||
|
||
### CLI embedding: tmux capture-pane polling
|
||
|
||
The `_CliToolPage` is a `QTextEdit` (read-only, dark theme, monospace font) that polls `tmux capture-pane -t <session>::<tool_id> -p -S -200` every 250 ms (4 Hz) and renders the captured output. The poll is driven by a `QTimer`; the captured content is compared to the current `toPlainText()` and only written if it changed (avoids flicker). The cursor is auto-scrolled to the bottom on each refresh.
|
||
|
||
The terminal is **read-only** — input is not supported. This is a deliberate scope limit: building a full PTY emulator is a separate project, and the user's stated use case ("peek orchestration") is read-only monitoring. For interactive input, the user should use a real terminal app and `tmux attach -t <session>::<tool_id>`.
|
||
|
||
The page exposes a `stop_polling()` method that the parent `WorkspaceTab` calls when the sub-tab is closed — this prevents the `QTimer` from outliving the page widget.
|
||
|
||
### Placeholder for passive / library tools
|
||
|
||
The `_PlaceholderPage` is shown for tools that have no interactive surface (no web + no CLI). It explains that the tool "is a passive/library tool with no interactive surface. It runs in the background and is consumed by other tools." This is informational only — there is no Start button because the tool's launcher type is `passive` (no `systemd` / `tmux` / `desktop` / `lxc`).
|
||
|
||
### Placeholder for not-yet-running tools
|
||
|
||
When a web or CLI tool is staged but not running, the sub-tab shows a `_PlaceholderPage` with a **Start tool** button. The button emits `start_tool_requested(str)`, which the main window connects to a handler that finds the matching `ServiceRow` and calls `start_service()`. After 1.5 s the workspace tab auto-refreshes so the placeholder → live view switch happens automatically.
|
||
|
||
### Sub-tab labels
|
||
|
||
Each sub-tab is labeled with an emoji prefix + tool_id:
|
||
|
||
- 🌐 — web-interface tool
|
||
- ⌨ — CLI tool
|
||
- 📦 — passive / library tool
|
||
- ⏸ suffix — tool is staged but not yet running
|
||
|
||
The emoji encoding lets the user scan the tab bar at a glance and see what kind of surface each sub-tab provides.
|
||
|
||
### Sub-tab closing
|
||
|
||
Sub-tabs are closable via the standard × button. Closing a sub-tab calls `stop_polling()` on the page (for CLI tools) but does **NOT** stop the underlying tool — that's the user's call from the Stack Editor. The empty-state placeholder is restored when all sub-tabs are closed.
|
||
|
||
## Refresh model
|
||
|
||
`WorkspaceTab.refresh()` is called from:
|
||
|
||
1. `_populate_services` (after the Stack Editor recompiles the active set)
|
||
2. Nav-click on the Workspace entry in the sidebar
|
||
3. 1.5 s after a "Start tool" click (so the placeholder → live view switch happens)
|
||
|
||
The refresh is **destructive** — all sub-tabs are torn down and rebuilt. This is simpler than diffing the active set, and the user's mental model is "refresh = rebuild." A future improvement would be to preserve sub-tab order and only add/remove the delta.
|
||
|
||
## Consequences
|
||
|
||
### Positive
|
||
|
||
- Every active tool is reachable from a single window — no context-switch to a browser or terminal app.
|
||
- Web tools embed directly via QWebEngineView, which is the standard PySide6 solution and ships with most PySide6 installs.
|
||
- CLI tools attach to the existing tmux session that the runtime executor already manages — no new process lifecycle to worry about.
|
||
- The "Start tool" button on placeholder pages bridges the gap between staging and running without requiring the user to switch to the Stack Editor.
|
||
- The servo-swap path is documented so a future servo migration is a one-line change.
|
||
|
||
### Negative
|
||
|
||
- The CLI embedding is read-only. Users who want interactive input must still use a real terminal app. This is a deliberate scope limit, not a bug.
|
||
- The `tmux capture-pane` poll runs every 250 ms per CLI sub-tab. With 10 CLI tools open, that's 40 Hz of subprocess calls — measurable but not heavy. A future improvement would be to pause polling when the sub-tab is not visible.
|
||
- The web embedding depends on `QtWebEngine`, which is a separate package on some Linux distros. The fallback placeholder handles the missing-package case gracefully.
|
||
- The destructive refresh model means sub-tab order is not preserved across refreshes. A future improvement would be to preserve order.
|
||
|
||
### Neutral
|
||
|
||
- The emoji prefixes (🌐 ⌨ 📦 ⏸) are not accessible to screen readers. A future improvement would be to add `setAccessibleName` on each sub-tab.
|
||
|
||
## Alternatives considered
|
||
|
||
### Open tools in an external browser / terminal
|
||
|
||
Rejected — this is the v3.0 behavior the user explicitly asked to replace. The whole point of the Workspace tab is to eliminate the context-switch.
|
||
|
||
### Use servo instead of QWebEngineView
|
||
|
||
Rejected as the default because servo's Python bindings are not production-ready and have no first-class PySide6 integration. The module is structured so servo can be swapped in later as a one-line change to `_make_web_view()`.
|
||
|
||
### Build a full PTY emulator for CLI tools
|
||
|
||
Rejected as scope creep. The user's stated use case is "peek orchestration" — read-only monitoring. A full PTY emulator is a separate project. For interactive input, the user should `tmux attach` from a real terminal.
|
||
|
||
### One workspace per tool (separate windows)
|
||
|
||
Rejected — this would re-create the multi-window juggling the app is supposed to eliminate. The single-window, multi-tab model matches the virt-manager / aqemu reference the user cited.
|
||
|
||
## Future work
|
||
|
||
- Preserve sub-tab order across refreshes.
|
||
- Pause `tmux capture-pane` polling when the sub-tab is not visible.
|
||
- Add `setAccessibleName` to each sub-tab for screen-reader support.
|
||
- Add a "Detach" button on each sub-tab that opens the tool in an external browser / terminal (for when the user does want a separate window).
|
||
- Swap in servo for web embedding once servo's Python bindings are production-ready.
|
||
- Add interactive input to the CLI embedding (would require a real PTY, e.g. via `QProcess` + `QTerminal` or a third-party widget).
|