feat: Fleet Panel & Profile UI (#350) - #362
Draft
jeonghun-jj-lee wants to merge 57 commits into
Draft
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
…tegration (#352) - Register amicode.fleet webview view in package.json (activity bar, no when gate) - Implement FleetPanelView (WebviewViewProvider) with typed postMessage protocol - Add fleet_webview.ts entrypoint + media/ui/views/fleet.ts view skeleton - Add fleet status bar item (click focuses Fleet panel) - Wire registerFleetPanel into extension activation - Add fleet_webview.js as esbuild browser bundle target - Standalone empty state: section headers for Topology, Profiles, Stats Closes #352
- Implement fleet_topology.ts component: star-layout SVG, solid/dotted lines, node badges (healthy/degraded/unreachable), 'This machine' marker - Add fleet_topology_data.ts: pure topology builder from FleetConfig - Integrate topology graph into fleet view (replaces text placeholder) - Click a node shows popover (hostname, role, health, sessions, last-seen) - Popover dismissed on click-outside or Escape - computeNodePositions: server at center, clients evenly spaced around it Closes #353
- Implement fleet_profiles.ts: TOML read/write/list/delete/duplicate, slug generation, validation (name + model required, slug uniqueness) - Add fleet_profile_manager.ts: watches profiles dir, pushes updates to panel, handles create/edit/duplicate/delete commands - Add fleet_profiles_view.ts: webview component with profile rows (name, model, play button, overflow menu with edit/duplicate/delete) - Inline create/edit form in the panel - Wire profile CRUD commands into extension activation - Round-trip: create → close → reopen preserves all fields (TOML serialization) Closes #356
- Implement fleet_wizard.ts: SSH executor, pre-flight checks (connectivity, binary, port), remote server config (fleet.json, token, launchd/systemd), local client config (fleet.json, token, tunnel plist) - Fleet lifecycle: create fleet (remote or local server), add machine, remove machine (revert to standalone), dismantle fleet - Service file generators: launchd plist + systemd unit for server, tunnel launchd plist with ServerAliveInterval=15, CountMax=2, TCPKeepAlive - Wizard commands registered: createFleet, addMachine, removeMachine, dismantle - Pre-flight checklist: SSH reachable → binary present → port available - Failure at any step is actionable (specific error + suggested fix) - Token generated with crypto.randomBytes(32) at 0600 permissions - Create Fleet button enabled in standalone mode Closes #354
- Implement fleet_launch.ts: buildLaunchRecord (FleetRecord in spooling state), writeFleetRecord (atomic tmp+rename), launchFromProfile convenience wrapper - Session ID: ses_ + 12 hex chars (crypto.randomBytes) - Aggregate stats: computeFleetStats from records (active/running/blocked/tokens) - Sweep: sweepCrashed marks running/blocked sessions with dead pids as crashed (pid-liveness probe via process.kill(pid, 0)), never kills processes - Register launch + sweep commands; push initial stats on activation - Stats section in panel updates on launch/sweep Closes #357
…#355) - Implement fleet_host_settings.ts: read/write server config over SSH (clients) or locally (servers), validate settings, detect restart-required - Settings: dbPath, port, binaryPath, logDir — all validated (absolute paths, port 1024-65535) - Restart-required detection: port, dbPath, binaryPath changes need restart; logDir does not - restartServer: stops + starts launchd/systemd service (over SSH or locally) - Atomic writes on the host (tmp + mv) Closes #355
…dler (#358) - Implement fleet_bridge.ts: FleetStateSnapshot builder, FleetAction parser, signal file enqueue (single-writer discipline, same as CLI verbs) - Add fleet-state (inbound) and fleet-action (outbound) to bridge protocol - Debounced fleet state watcher (500ms, fs.watch on registry dir) - Bridge allowlist: add amicode.fleet.steer/stop/reTier commands - chat_bridge.ts: handle fleet-action messages from the app (parse + dispatch) - Register bridgeAction command to route app actions into signal files - Fleet state watcher pushes stats updates to the panel on record changes Closes #358
… in standalone - Create Fleet button now appears immediately below the STANDALONE badge - Topology section (SVG graph + header) hidden entirely in standalone mode — no empty 180px viewBox taking space for one line of text - Topology section appears only when fleet is active (server/client role)
Create Fleet now asks how the server should run amicode: - **Development clone**: clones harmoniqs/opencode + harmoniqs/amicode on the remote, installs deps, builds from source, codesigns the binary, installs a rebuild_amicode.sh script on the host for future updates, and points the server service at the dev-built binary. Pre-flight checks git/node/pnpm/bun. - **Release binary**: uses whatever opencode binary is already installed on the remote (existing behavior). The rebuild script (installed at ~/harmoniqs/rebuild_amicode.sh on the server) mirrors the user's local script: pulls both repos, builds, codesigns, restarts the fleet server service, and backs up/restores session DBs. Server launchd plist and systemd unit now accept a custom binary path instead of hardcoding /usr/bin/env opencode.
After fleet creation, if local session databases exist at ~/.local/share/opencode/, the wizard offers to merge them into the fleet server: - Discovers local DBs (opencode*.db + WAL/SHM sidecars) - If server has NO sessions: clean copy via SCP (all DBs transferred directly) - If server HAS sessions: copies with .local-merge.db suffix, then runs sqlite3 INSERT OR IGNORE per table to merge without duplicates - If sqlite3 not available on server: leaves .local-merge.db files for manual merge - Shows file count and total size before asking Fixes the correct path: ~/.local/share/opencode/ (not ~/.config/opencode/).
Session DB path is now resolved dynamically instead of hardcoded: - Local: resolveSessionDbDir() checks $XDG_DATA_HOME/opencode, falls back to ~/.local/share/opencode (mirrors opencode's core/src/global.ts logic) - Remote: resolveRemoteSessionDbDir() SSHs in and echoes the resolved path using the remote machine's $XDG_DATA_HOME This ensures the merge finds the DBs regardless of custom XDG configuration on either machine.
Implements semver + capability negotiation between client and server:
- fleet_compat.ts: checkCompatibility() determines state from versions +
capabilities. Three states:
- Compatible: same major version, all capabilities known
- Degraded: server newer or has unknown capabilities — basic features work,
panel shows hint to rebuild extension
- Incompatible: major version mismatch — panel shows warning with
Go Standalone action
- Server writes ~/.amico/ops/fleet/server_version.json on setup and after
every rebuild (rebuild_amicode.sh updated to write it). Contains version,
schema, and advertised capabilities list.
- Client probes server_version.json on activation (via SSH). If degraded,
shows a non-blocking amber banner. If incompatible, shows a red banner
with a one-click Go Standalone escape hatch.
- Capabilities are additive (KNOWN_CAPABILITIES list). Client ignores
capabilities it doesn't understand — they just show as 'degraded' until
the extension is rebuilt.
- Records stay forward-compatible: schema field on every record, unknown
fields ignored by older readers.
Replaces GitHub round-trip with direct SSH push to the host's repos. The full reconnect cycle is now: 1. Push local commits → host (git push fleet-host <branch>) 2. Trigger host rebuild (rebuild_amicode.sh absorbs the new commits) 3. Pull host state → client (builds + sessions) In normal fleet mode (no local changes): steps 1-2 are skipped. Implementation: - fleet-host git remote added to local repos pointing at host:~/harmoniqs/* - Host repos configured with receive.denyCurrentBranch=updateInstead (set during dev-clone provisioning) - pushToHost(): checks rev-list count vs fleet-host, pushes if ahead - triggerHostRebuild(): runs rebuild_amicode.sh on the host over SSH - syncFromHost(): the full cycle — push first, rebuild, then pull - ensureFleetRemote(): one-time setup of the fleet-host remote No internet needed — only the SSH tunnel between client and host.
When a sync push is rejected (non-fast-forward / diverged branches), the sync detects the conflict and offers to launch an Amicode chat session to resolve it — instead of dumping a git error. The conflict resolution prompt gives the agent full context: - Which repo (amicode/opencode) - Local and host SHAs - The three resolution options (rebase, force-push, reset) - Commands to inspect the divergence Flow: 1. Auto-sync detects push rejection → banner shows 'conflict' 2. VS Code warning asks: 'Resolve with Amicode' or 'Dismiss' 3. If resolve: opens a new chat with the resolution prompt pre-loaded 4. Agent inspects the git state and resolves (rebase, merge, etc.) 5. User re-runs sync manually after resolution Also: - Manual 'Sync from Host' command (amicode.fleet.sync) added - Auto-sync on activation offers 'Reload Window' if extension was updated - Replaces the old compat probe (version negotiation is superseded by the sync model — versions are always the same after sync)
When a sync conflict is detected:
1. Warning notification tells the user what happened
2. 'Resolve with Amicode' opens a NEW chat tab (visible, interactive)
3. The conflict resolution prompt is posted as a draft message
4. A second notification tells the user which session was opened:
'Conflict resolution session launched in "Amicode Chat 2"'
5. User reviews the draft, sends it, and interacts with the agent
Uses ChatPanel.openNew() + postDraftMessage() instead of the fragile
executeCommand('amicode.sendMessage') approach. The draft-message bridge
kind posts the prompt to the iframe for the app to populate the input.
Adds detectLocalRepos() which finds the amicode and opencode repo roots by walking up from ctx.extensionPath and the opencodeBinary setting respectively, looking for .git. No hardcoded paths — falls back to ~/harmoniqs/* only if detection fails. This is the foundation for eliminating hardcoded paths in provisioning and sync. Next step: wire it into provisionDevClone + fleet_sync.
All repo path references in fleet_sync.ts now use detectLocalRepos() which resolves from the running build context. No more hardcoded ~/harmoniqs/amicode or ~/harmoniqs/opencode.
…t push Rewrites provisionDevClone to: - Detect local repo paths from the running build (detectLocalRepos) - Push directly from local to host over SSH (no GitHub clone) - Accept a configurable remoteRoot (defaults to ~/harmoniqs) - Auto-detect current branches from the local repos The host gets the client's EXACT state — same branch, same SHA. All remote-side paths use the `root` variable, not hardcoded ~/harmoniqs.
The Create Fleet wizard now detects repo paths from the running build: - amicode repo: found by walking up from ctx.extensionPath - opencode repo: found by walking up from amicode.opencodeBinary setting No hardcoded paths in the provisioning flow.
After creating a fleet, the extension is still connected to the old local server. A reload is needed so the fleet guard blocks local spawn and the extension connects through the tunnel to the host instead.
The initial push of the opencode repo (~530 MB .git) easily exceeds 60s on anything less than gigabit. Bump to 600s (10 min) for the first push; subsequent pushes are incremental and fast.
Replace the sequential QuickPick wizard with a skill document that guides the agent through fleet creation, machine add/remove, dismantle, and reconfiguration. The skill documents exact SSH commands, error handling patterns, service templates, and the validation checklist. Part of #363.
) - Remove fleet_wizard.ts entirely - Extract shared SSH/detection utilities into fleet_ssh.ts (used by fleet_sync, fleet_compat, fleet_host_settings) - Replace QuickPick createFleet/addMachine/removeMachine/dismantle commands with ChatPanel.openNew + postDraftMessage (launches a chat session where the agent handles the lifecycle via the fleet skill) - Remove unused readTopology import from extension.ts Build passes: tsc --noEmit clean, esbuild bundle succeeds.
The wizard module was deleted — its tests no longer apply. The lifecycle operations are now handled interactively by the agent through the fleet skill; the remaining 117 fleet tests all pass.
#363) openOrReveal + postDraftMessage keeps the user in their current Amicode Chat — the fleet prompt appears as a draft in the input, ready to send. No extra tab spawned.
) The Create Fleet button was doing nothing because opencodeReadyUrl was undefined (server still booting). Now launchFleetChat falls back to: 1. Open the main chat (triggers server boot / loading state) 2. Poll for opencodeReadyUrl every 500ms (30s timeout) 3. Post the fleet draft once the server is ready Also adds tests proving the webview→command dispatch chain works.
The webview relay script (extension→iframe lane) only forwarded messages with specific 'kind' values. 'draft-message' was missing from the allowlist, so postDraftMessage was silently dropped — the fleet button's prompt never reached the app.
…ostDraftMessage (#363) The app never handled the 'draft-message' kind — it was dead code. The actual mechanism: the app's /new-session route reads a ?prompt= search param and pre-fills the composer input. New approach: - Add a 'navigate' message kind to the webview relay (sets iframe.src) - Add ChatPanel.navigateToPath() which posts this message - launchFleetChat now navigates to /new-session?prompt=<encoded> This actually opens a new session with the fleet prompt pre-filled.
Setting iframe.src bare lost the boot params (auth_token, colorScheme, amicode_hide_project). Now the relay builds the new URL from the existing iframe src, carrying over all boot params the app needs.
Instead of reloading the iframe (loses auth) or posting a dead
draft-message, navigate the SPA client-side:
1. Extension relay posts {kind:'navigate', path} into the iframe
2. App's AmicodeThemeBridge handles it: history.pushState + popstate
3. SolidJS Router picks up the URL change, renders /new-session
The prompt param pre-fills the composer input (existing app feature).
The SPA router doesn't respond to pushState/popstate directly — the
TabsProvider owns navigation. Add AmicodeNavigateBridge inside TabsProvider
that listens for {kind:'navigate', path:'/new-session?prompt=...'} and
calls tabs.newDraft() to properly create a draft tab with the prompt
pre-filled.
Verified end-to-end:
- Extension test: navigateToPath posts correct message to webview
- Relay: forwards navigate kind to iframe
- App: AmicodeNavigateBridge → tabs.newDraft → creates tab + navigates
The skill resolver requires YAML frontmatter with surface:public to include a skill in the session's available set. Without it, the fleet skill was invisible to the agent.
Skills are registered as slash commands on the server. Prefixing the prompt with /fleet makes the app invoke the fleet skill as a command (via POST /session/:id/command), which loads the skill automatically on the first turn — same pattern as /report-a-bug.
The retry pattern (post now + 1.5s) was causing double session creation since newDraft/createSession aren't idempotent. Post once only.
…to-send (#363) - Rename skill from 'fleet' to 'create-a-fleet' (dir + frontmatter) - Update extension prompts to /create-a-fleet - autoSend now uses a global signal set by the navigate bridge before newDraft, read by the draft controller on mount
…ng skill (#363) All fleet issues (sync conflicts, tunnel down, drift, SSH failures, sync errors) now surface a VS Code warning notification with a 'Resolve with Amico' button that opens a new auto-sending chat session routed to the /fleet-troubleshooting skill with structured diagnostic context. Changes: - Add FleetIssue type + notifyFleetIssue() as the single dispatch point - Delete broken launchConflictResolutionChat() (used openNew + postDraftMessage which never auto-sent) - Refactor all 5 notification sites to use notifyFleetIssue(): - Auto-sync conflict (was inline showWarningMessage + broken chat launch) - Manual sync conflict (same) - Tunnel down after 5 checks (was 'Go Standalone' only) - Fleet drift on activation (was 'Fix fleet' terminal) - Sync errors (was panel-only, no notification) - New skill: /fleet-troubleshooting (SKILL.md with diagnosis + resolution for all 5 issue kinds) - Cross-reference between /create-a-fleet and /fleet-troubleshooting
The fleet rebuild template pointed at ~/.config/opencode/ for the session DB backup, but the DB lives at ~/.local/share/opencode/ (XDG_DATA_HOME). The script was backing up 0-byte ghost files from the wrong location.
jeonghun-jj-lee
force-pushed
the
amico/issue-350-fleet-panel
branch
from
August 13, 2026 18:21
659db56 to
13501a4
Compare
…code.sh, drop rebuild-script step - Step 8 is now 'Copy session databases to server' (was Step 16) — sessions must be in place before the build or service start - Removed the inline rebuild_amicode.sh generation (Step 14) — the fleet model is push-then-build; local_rebuild_amicode.sh is scp'd when needed, not baked into fleet creation - Renumbered steps 8-16 cleanly
In fleet-client mode the extension reads the tunnel port from fleet.json (canonical.port), not from the VS Code setting. The port setting is the user's standalone preference and must survive fleet enrollment unchanged so the local session isn't killed mid-transition. - extension.ts: remove opencodePort reset on go-standalone - fleet_health.ts: drop port check from checkFleetSettings - fleet_health.test.ts: update tests (port is irrelevant) - tools/fleet/install.sh: only set binary, never port
The question tool schema requires the `options` key on every question object, even for kind: "text" free-form inputs. The agent was omitting it, causing SchemaError(Missing key at ["questions"][0]["options"]).
- Consolidate duplicate fleet status bar items into one role-aware StatusBarManager.fleetItem (command switches between goStandalone in client mode and fleetPanel.focus otherwise) - Remove orphaned fleetStatusItem from extension.ts - Reorder fleet create skill: tunnel install + verification before local fleet.json write (the point-of-no-return that triggers the VS Code server switch), preventing session drops mid-setup - Add GATE steps requiring server/tunnel health confirmation before proceeding to the local role switch
The skill was tagged surface:internal but lives in the in-repo library root (which admits public only), making it unreachable from either staging path. create-a-fleet cross-references it and fleet notifications auto-trigger it — it belongs in the public surface.
Add Fleet Profile and Fleet-managed session to the domain glossary. ADR 0006 records the architectural decisions: VS Code native webview, activity bar placement, panel-first delivery, profile TOML storage, and bridge-push for dropdown enrichment.
…move Two bugs: 1. Tunnel plist had no EnvironmentVariables.PATH, so SSH ProxyCommands referencing binaries outside /usr/bin (e.g. tailscale) silently failed. 2. /fleet remove wiped canonical coordinates from fleet.json, making reconnection impossible without a full re-setup.
…nical When fleet.json has role=standalone but canonical coordinates are present, the panel now shows 'Reconnect Fleet' instead of 'Create Fleet'. This distinguishes 'never had a fleet' from 'disconnected from a fleet'. The host sends hasCanonical on the role message; the webview switches button text and action accordingly.
Root cause: goStandalone() constructed a fresh config object, discarding the existing canonical field. Now it spreads the existing config first, so canonical survives a standalone switch. Only 'dismantle' should remove canonical entirely. Test added: verifies canonical persists through goStandalone.
- Reconnect Fleet: writes role=client, loads tunnel plist, stops local server, polls canonical until it responds, then connects SSE + chat. No window reload needed. - Go Standalone was already seamless (no change needed there). - Removed duplicate reconnectFleet registration that did a window reload. Verified: tsc clean, fleet_fallback tests pass (7/7), extension builds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fleet Panel & Profile UI
Implements the full Fleet Panel (#350) across all 7 sub-issues, merged sequentially into this integration branch:
Commits (by sub-issue)
Fleet Panel: WebviewViewProvider shell + status bar integration #352 — WebviewViewProvider shell + status bar integration
amicode.fleetwebview view (activity bar, nowhengate)Fleet Panel: SVG topology graph with clickable nodes #353 — SVG topology graph with clickable nodes
Fleet Panel: Fleet Profiles CRUD #356 — Fleet Profiles CRUD
~/.amico/ops/fleet/profiles/Fleet Panel: setup wizard + fleet lifecycle (create/add/remove/dismantle) #354 — Setup wizard + fleet lifecycle
Fleet Panel: session launch from profile + aggregate stats #357 — Session launch from profile + aggregate stats
spoolingstateFleet Panel: remote host settings (DB path, port, binary, logs) #355 — Remote host settings (DB path, port, binary, logs)
Fleet: sessions dropdown enrichment (bridge + icon + context menu) #358 — Sessions dropdown enrichment (bridge protocol)
fleet-state(extension → app) +fleet-action(app → extension)Test Results
opencode_dev.test.ts— lockfilesource: localin dev mode)tsc --noEmitpasses)Closes #350, #352, #353, #354, #355, #356, #357, #358