Skip to content

feat: Fleet Panel & Profile UI (#350) - #362

Draft
jeonghun-jj-lee wants to merge 57 commits into
mainfrom
amico/issue-350-fleet-panel
Draft

feat: Fleet Panel & Profile UI (#350)#362
jeonghun-jj-lee wants to merge 57 commits into
mainfrom
amico/issue-350-fleet-panel

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

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)

  1. Fleet Panel: WebviewViewProvider shell + status bar integration #352 — WebviewViewProvider shell + status bar integration

    • amicode.fleet webview view (activity bar, no when gate)
    • Fleet status bar item (click → focus panel)
    • Typed postMessage protocol (host ↔ webview)
    • esbuild browser bundle target
  2. Fleet Panel: SVG topology graph with clickable nodes #353 — SVG topology graph with clickable nodes

    • Star-layout SVG (server center, clients around)
    • Solid/dotted connection lines (connected/offline)
    • Node badges (healthy/degraded/unreachable)
    • Click popover (hostname, role, health, sessions)
  3. Fleet Panel: Fleet Profiles CRUD #356 — Fleet Profiles CRUD

    • TOML storage under ~/.amico/ops/fleet/profiles/
    • Create/edit/duplicate/delete with inline forms
    • Slug generation, uniqueness enforcement, validation
    • fs.watch auto-refresh on external changes
  4. Fleet Panel: setup wizard + fleet lifecycle (create/add/remove/dismantle) #354 — Setup wizard + fleet lifecycle

    • SSH-based fleet creation (pre-flight checks → configure → validate)
    • Create fleet, add machine, remove machine, dismantle
    • launchd/systemd service generators
    • Fleet token (crypto.randomBytes, 0600 permissions)
  5. Fleet Panel: session launch from profile + aggregate stats #357 — Session launch from profile + aggregate stats

    • Play button → FleetRecord in spooling state
    • Aggregate stats (active/running/blocked/tokens today)
    • Sweep: mark orphaned crashed sessions (pid-liveness probe)
  6. Fleet Panel: remote host settings (DB path, port, binary, logs) #355 — Remote host settings (DB path, port, binary, logs)

    • Read/write over SSH (clients) or locally (servers)
    • Validation (absolute paths, port 1024-65535)
    • Restart-required detection per setting
  7. Fleet: sessions dropdown enrichment (bridge + icon + context menu) #358 — Sessions dropdown enrichment (bridge protocol)

    • fleet-state (extension → app) + fleet-action (app → extension)
    • Debounced state watcher (500ms, fs.watch)
    • Signal file enqueue (single-writer discipline)
    • Bridge allowlist additions

Test Results

  • 88 new tests across 7 test files, all passing
  • Full suite: 1024/1025 pass (1 pre-existing failure in opencode_dev.test.ts — lockfile source: local in dev mode)
  • TypeScript: clean (tsc --noEmit passes)

Closes #350, #352, #353, #354, #355, #356, #357, #358

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6a80a07-2e05-4eab-99b7-c3e08633abbb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

…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
jeonghun-jj-lee force-pushed the amico/issue-350-fleet-panel branch from 659db56 to 13501a4 Compare August 13, 2026 18:21
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fleet Panel & Profile UI

1 participant