feat: AgentField Desktop (Mac-first) + Windows enablement, bundled CLI, tray, deep links#752
Draft
AbirAbbas wants to merge 27 commits into
Draft
feat: AgentField Desktop (Mac-first) + Windows enablement, bundled CLI, tray, deep links#752AbirAbbas wants to merge 27 commits into
AbirAbbas wants to merge 27 commits into
Conversation
Docker-Desktop-style dashboard for non-technical users: shows control plane health (GET /health) and the locally installed agent nodes from ~/.agentfield/installed.yaml, cross-checked against GET /api/v1/nodes for a running/stopped/unknown badge per agent. Polls every 5s with a manual Refresh button and graceful empty states. Electron + electron-vite + React + TypeScript, plain CSS. Secure defaults: contextIsolation on, nodeIntegration off, sandboxed renderer, single contextBridge API. All data access is isolated in src/main/agentfield.ts with a marked seam to later swap the registry read to `af list -o json`. 29 vitest unit tests cover registry parsing, health mapping, and badge derivation. Self-contained under desktop/ (own package.json); no packaging (electron-builder) yet, and the GUI is untested in this headless environment — typecheck, production build, and unit tests all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the stale commented-out windows block with a real agentfield-windows-amd64 build mirroring the linux/darwin ones (goreleaser appends .exe on its own). Groundwork only: the release workflow's build matrix filters by --id and does not build this id yet; shipping the artifact needs a follow-up matrix entry (windows runner, or mingw-w64 on the linux runner for the CGO sqlite dependency). Also modernizes archives.builds/format to ids/formats so `goreleaser check` passes clean again (both were deprecated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`af stop` used process.Signal(os.Interrupt) and a signal-0 probe, both unsupported on Windows (and os.FindProcess always succeeds there, so the liveness check was meaningless). Extract the two process operations into build-tagged helpers: proc_unix.go keeps the existing SIGINT + signal-0 behaviour; proc_windows.go uses taskkill for the graceful request and a tasklist PID query for liveness. stop.go changes are limited to swapping the two call sites and the now-unused syscall import. Windows paths are compile-verified only (GOOS=windows cross-build), not yet tested on a real Windows machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`af logs` shelled out to tail(1), which does not exist on Windows. The tail/follow commands now go through one tailCommand helper: unchanged tail(1) invocations on Unix, PowerShell Get-Content -Tail (-Wait for follow) on Windows, with proper single-quote escaping of the log path. Windows path is compile-verified only, not yet run on a real machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Go agent nodes declare unix-style binary paths in their manifests (entrypoint.start: bin/foo). On Windows the install-time `go build -o` output now carries the conventional .exe extension, and the runner's GoBinaryProgram resolves an extensionless start path to the built .exe when present. No behaviour change on other platforms; windows path is compile-verified only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
…ed helpers The patch-coverage gate flagged the runtime.GOOS-gated windows branches (PowerShell tail construction in logs.go, .exe naming/resolution in gointerp.go) as untestable on linux CI. Extract each into a pure helper taking an explicit goos string — tailCommandArgs, withExeSuffixFor, goBinaryProgramFor — with the exported wrappers passing runtime.GOOS, so behavior is unchanged while both platform paths are unit-testable anywhere. Table-driven tests cover the windows tail command (incl. single-quote escaping), .exe suffixing, and the built-.exe fallback resolution; refactored regions now profile with zero uncovered blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p on Windows control-plane/agentfield.yaml was a checked-in symlink to config/agentfield.yaml. On Windows checkouts (core.symlinks=false, the default) git materializes it as a plain text file containing the literal target path, which Viper then finds via its . search path and fails to parse: `cannot unmarshal !!str config/...` — a fatal error on every `af server` run from the control-plane directory. The symlink is redundant on every platform: both cmd/af and cmd/agentfield-server already search ./config and <execDir>/config, which resolve to the same file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The af binary only filled in DatabasePath/KVStorePath when storage.mode was absent entirely. A config file that sets mode: "local" while leaving the paths empty (which the repo's own sample config/agentfield.yaml does, and which af picks up automatically when running next to it) skipped the defaulting block and failed startup with "database path is empty". Default the paths whenever the effective mode is local, mirroring cmd/agentfield-server which already had this shape. Found on Windows but platform-independent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
exec.LookPath alone cannot pick a Python on Windows: stock machines ship Microsoft Store "app execution alias" stubs named python3.exe (and often python.exe) that resolve like real binaries but exit 9009 without running anything. resolveVenvInterpreter took the first PATH hit and only version-checked that one, so a node declaring requires-python failed with "no compatible interpreter" even when a perfectly good python was next in line. Verified live on Windows 11: python3 -> dead Store stub (exit 9009), python -> real 3.11.9, never consulted. ambientPythonInterpreter now probes each candidate by actually running it (-c version query) and takes the first that answers. The candidate list gains "py", the Windows launcher: python.org installers register it even when "add python to PATH" is left unchecked (the default), where it is the only working entry. It does not exist on Unix, so probing it there is a no-op. The legacy no-requires-python path in InstallPythonDependencies now uses the same probe instead of blindly running python3 -m venv and falling back, and fails with an actionable error listing the probed candidates when nothing on PATH runs. Test stubs that previously only handled -m venv now answer the -c version probe, matching how real interpreters behave; the venv-creation-failure contracts now expect the earlier, more actionable error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spawned agents log through stdout/stderr redirected to a log file. On Windows, Python then encodes with the legacy ANSI code page (cp1252), which cannot represent the SDK's emoji log prefixes - every heartbeat flooded the log with UnicodeEncodeError tracebacks from the logging module. Verified live: with PYTHONUTF8=1 the same agent logs cleanly. Applied in both spawn paths (services.buildProcessConfig, which backs af run, and the legacy packages runner). An explicit PYTHONUTF8 in the caller's environment wins. UTF-8 mode is a no-op where UTF-8 is already the default, so this is safe on every platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dows dev_service.go is build-tagged !windows (Windows gets a stub service), but two pieces of its test surface were not: - mockFileSystemAdapter lived in dev_service_test.go (!windows) while untagged test files (package_service_test.go, coverage_additional_ test.go) use it. Moved to a new untagged mocks_fs_test.go. - Three tests in the untagged coverage_additional_test.go exercised Unix-only DefaultDevService methods (loadDevEnvFile, startDevProcess, port helpers) that the Windows stub does not define. Moved into the !windows-tagged dev_service_test.go. Before this, go vet / go test of internal/core/services failed to compile on Windows. Also carries the one-line stub update from the interpreter-probe change (the fake python in coverage_additional_test.go now answers the -c version query); no other test logic changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…control plane
checkControlPlane treated any HTTP 200 from {baseUrl}/health as
"healthy", so anything squatting on the popular default port lit the
dashboard green. Found live on Windows: an unrelated dev server answering
{"status":"alive"} on /health showed as a running control plane.
The probe now recognizes an AgentField control plane by its health
payload shape (status: healthy|unhealthy, per routes_core.go). Anything
else reachable on the port reports recognized: false with an explanatory
error, renders as a yellow "Another service is on this port" state, and
is excluded from the nodes cross-check so a foreign /api/v1/nodes
response cannot corrupt agent badges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Data layer growth behind the same single-snapshot IPC: - fetchExecutions parses GET /api/ui/v2/workflow-runs into in-flight runs plus a short tail of finished ones. - fetchDashboardMetrics parses GET /api/ui/v1/dashboard/summary (agents running/total, runs today/yesterday, success rate). - Both are only consulted on a recognized control plane, so a foreign service on 8080 can't inject activity or metrics. Install flow, with the af CLI as the single contract: - src/shared/catalog.ts is a curated hard-coded list of installable nodes (the pre-marketplace seam; swap for a remote catalog fetch later). Entry names must equal the node's manifest name — that is the registry key the app uses to detect installed state (SWE-AF installs as "swe-planner"). - src/main/installer.ts spawns `af install <source>`, sanitizes ANSI/spinner output into displayable lines, and streams them to the renderer over agentfield:install-progress. The renderer only ever sends catalog names over IPC; unknown names are refused, raw sources never reach a shell. A missing af CLI degrades to an actionable message. Verified live on Windows: installed SWE-AF from the app end-to-end — including uv provisioning Python 3.12.13 because the node requires >=3.12 and the ambient interpreter is 3.11 (the interpreter-probing fix working in production). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native-feeling chrome: - No default File/Edit/View menu bar anywhere. macOS keeps the minimal app menu it needs for Cmd+Q/copy-paste; Windows/Linux drop the bar entirely (Menu.setApplicationMenu(null)). - Seamless titlebar: hiddenInset + sidebar vibrancy + traffic-light inset on macOS, hidden titlebar with the native control overlay on Windows. The sidebar rail and view header are draggable regions. - System font stack, light/dark from the OS, hairline borders. Layout: left sidebar (Dashboard / Agents / Activity / Install + a control-plane status pill pinned at the bottom), right content view. The Dashboard leads with stat tiles (agents running, executing now, runs today vs yesterday, success rate) over a recent-activity list; Agents and Activity render as clean row panels; Install streams per-row progress. macOS-specific chrome is behind platform guards and still needs one smoke run on a real Mac; everything else verified live on Windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`npm run dist` produces DMG+zip on macOS and a one-click NSIS installer on Windows into release/ (git-ignored); `npm run dist:dir` for a quick unpacked smoke. electron-builder is pinned to v25 — v26 requires require(ESM) support (Node 20.19+/22.12+) that older Node 22 lacks. Unsigned for now, default Electron icon; signing/notarization and a real icon come before distribution. Verified on Windows: the packaged AgentField.exe boots and opens its window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the default Electron icon everywhere. scripts/make-icons.mjs renders the brand mark (the exact outlined "af" + dot paths from the web UI logo, so no font dependency) via an offscreen Electron window into: - build/icon.icns — a real ICNS container (Apple-grid margins, baked shadow) that electron-builder ships on macOS - build/icon.png — the 1024px source electron-builder converts to the Windows/Linux app icon (verified: the packaged exe carries the mark) - resources/icon.png — runtime window/taskbar icon for win/linux - resources/tray/* — tray glyphs at 1x/1.5x/2x, active/inactive crossed with light/dark-taskbar variants; the gold dot doubles as the status light Outputs are committed (`npm run icons` regenerates), resources/** now ships inside the app package, and desktop/build is un-ignored for exactly the two icon files the root .gitignore would otherwise drop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tray (Windows/Linux only — macOS has af-tray, installed with AgentField itself): a status glyph whose brand dot goes gold while the control plane runs, tooltip + disabled menu row naming the state (running / unhealthy / port-in-use / stopped), Open AgentField / Open web UI / Quit. Closing the window now hides to the tray, Docker-Desktop style; presentation logic is pure in tray-model.ts and unit-tested. If tray creation fails (some Linux desktops) the app logs why and keeps classic quit-on-close. Deep links: the app registers the agentfield:// scheme (declared for macOS via electron-builder `protocols`, HKCU-registered at runtime on Windows) and holds the single-instance lock — a relaunch or an agentfield://dashboard|agents|activity|install URL focuses the running app and switches it to that view. Parsing is pure in shared/deeplink.ts (unit-tested); the view union now lives there as the one canonical list. Verified live on Windows (packaged build): deep link cold-open, deep link into a tray-resident app, view switching, single-instance focus, close-to-tray, and the port-in-use state against a foreign service squatting on 8080. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The macOS menu-bar tray now prefers the AgentField desktop app when it is installed: every "open" action first tries the agentfield:// deep link for the equivalent view (dashboard/agents/activity) and falls back to the web UI in the browser. Detection is the deep link itself — `open agentfield://…` exits non-zero fast when nothing registered the scheme, so there is no separate probe to drift. Page→view mapping and the browser fallback are pure helpers in shared.go with contract tests; the darwin file only gains the two-line try/fallback. assets/appicon.icns was a renamed 512px PNG, not an ICNS container — Finder/dock could show a generic icon. It is now a real icns (PNG members at 32…1024 on Apple's grid), generated by the same desktop/scripts/make-icons.mjs that produces the desktop app's icons, so both apps wear the same mark. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replacing the icon and context menu on every 5s poll churns native tray APIs for nothing and can dismiss a menu the user has open on Windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swept the org: a repo is installable iff it has agentfield-package.yaml at its root (the manifest `af install` requires) — that held for four of fifteen repos, and the catalog now carries all of them: swe-planner (SWE-AF), pr-af, sec-af, and cloudsecurity-af, each keyed by its manifest `name:` so installed-state detection keeps working. The sweep rule is documented at the top of catalog.ts for the next addition. Required secrets (e.g. OPENROUTER_API_KEY) are resolved at `af run` time, not install time, so installs from the app stream cleanly and setup prompts happen where a terminal exists. Verified in the packaged app: all four render, swe-planner shows Installed ✓. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agents view: Start / Stop / Restart per row, shelling out to the af CLI (`af run` / `af stop`; restart is stop-then-run — the CLI has no restart verb). Names are validated against installed.yaml before anything is spawned; the renderer only ever sends names. Settings view: open at login (OS login item, packaged builds only — launches hidden with --hidden, tray-only), start the control plane automatically, and per-agent auto-start switches. Persisted to settings.json in userData, normalized on load so hand-edits and old shapes can't break the app. Autostart on every launch (src/main/autostart.ts, planning pure and unit-tested): spawn `af server` detached (logs to ~/.agentfield/logs/control-plane.log, same file macOS launchd uses) only when nothing answers on the port — never over a live control plane or a foreign service — then start the selected agents. Agents whose registry entry went stale (running with no control-plane presence, e.g. after a reboot; Windows never reconciles that live) are restarted, not skipped. The point: agents are already answering when Claude/Codex/anything queries them — nobody has to start a server first. Also fixes a deep-link race this surfaced: a link that cold-starts the app pushed the view at did-finish-load, before React subscribed, and got dropped. The renderer now announces readiness and collects the pending view (agentfield:renderer-ready), so agentfield://settings into a hidden app lands on Settings. Verified live on Windows (packaged build): start/restart/stop from the UI (badge, port, registry all agree), settings round-trip, login-item registration with --hidden, hidden tray-only launch, autostart bringing a stopped agent up on relaunch, and the port-in-use guard skipping the control-plane start while a foreign service owns 8080. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Non-technical users install ONLY the desktop app and get all of AgentField: the package now carries the af CLI (extraResources from vendor/, staged by `npm run bundle-cli` or the release pipeline). Every launch resolves which af to drive (src/main/cli.ts): managed (~/.agentfield/bin — the same location the curl installer uses, so the two installers converge instead of double-installing) → PATH → bundled. A copy older than MIN_AF_VERSION is skipped — the app runs on its bundled CLI meanwhile, and Settings grows an "AgentField CLI" card showing version + source with an Update button that installs the bundled copy into the managed location (never over a newer one; dev builds are trusted). With no CLI anywhere, first launch auto-provisions ~/.agentfield/bin (agentfield + af alias, curl-installer naming), copes with a running binary via rename-aside, and registers the Windows user PATH so terminals get `af` too. Launches also run `af skill install --non-interactive` (Settings toggle, default on) so detected coding agents — Claude Code, Codex, Gemini … — always know how to use AgentField; skillkit's state file makes this idempotent and shared with the curl installer. Verified live on Windows from a simulated fresh machine (no managed copy, no PATH af): first launch provisioned both binaries, registered the user PATH, installed the skill into ~/.claude/skills, resolved to the managed copy, and drove agent start/stop through it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two blind spots closed: - The desktop app had no CI at all — its tests only ever ran on a dev machine. New desktop.yml runs typecheck, vitest, and an unsigned electron-builder package on macos-14 and windows-latest (a stub in vendor/ validates the bundled-CLI extraResources wiring), so the platform-guarded chrome and packaging config prove out on both ship targets per PR. - cmd/af-tray's darwin implementation is CGO code that Linux CI never compiles (it builds the !darwin stub, and the CGO_ENABLED=0 matrix can't touch it) — a type error in tray_darwin.go would only surface at release time. control-plane.yml grows a macos-14 job that builds and vets the tray, wired into required-checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
AgentField Desktop + Windows enablement
A Mac-first desktop companion (Docker-Desktop analog) plus the Windows fixes that make the whole stack run natively there. Everything below is runtime-verified on real Windows 11 (packaged builds, not dev mode); macOS is compile/package/test-verified in CI on macos-14 and needs one manual smoke (checklist at the bottom).
The app
Views — Dashboard (stat tiles + recent activity), Agents (status badges + Start / Stop / Restart per row), Activity (in-flight runs with live pulse + recent tail), Install (curated catalog: every Agent-Field org repo with an
agentfield-package.yaml— swe-planner, pr-af, sec-af, cloudsecurity-af — one-clickaf installwith streamed progress), Settings.Mac-first chrome — no menu-bar clutter (minimal app menu on macOS only), hiddenInset titlebar + traffic-light inset + sidebar vibrancy (macOS) / native control overlay (Windows), system font, light/dark from the OS, brand "•af" icon set rendered from the web-UI logo paths (real ICNS, exe icon, window icon, tray glyphs —
npm run iconsregenerates).Autopilot (the point: agents are already answering when Claude/Codex asks) —
af-trayas its menu-bar companion.agentfield://dashboard|agents|activity|install|settingsdeep links + single instance; the macOSaf-traynow opens the desktop app via deep link when installed and falls back to the web UI (the failedopenis the detection).Bundled CLI — the package carries
af(extraResources;npm run bundle-clistages it). Per-launch resolution: managed~/.agentfield/bin(same location/names as the curl installer → no double install) → PATH → bundled, with a MIN_AF_VERSION gate and an "Update AgentField" button in Settings. Fresh machines get auto-provisioned (both binary names, Windows user PATH registered). Launch also runsaf skill install --non-interactive(toggleable) so detected coding agents always know AgentField.Windows enablement (control plane)
control-plane/agentfield.yamlremoved (materializes as a broken text file on Windows and kills viper).cmd/af/main.go).python3stub answering exit 9009) — run-probes candidates instead of LookPath, with uv provisioning verified in production during a real SWE-AF install.PYTHONUTF8=1for spawned agents (cp1252 + emoji log spam), on both spawn paths.af-tray (macOS)
Deep-links into the desktop app with browser fallback;
assets/appicon.icnswas a renamed PNG, now a real ICNS wearing the same mark.CI added
desktop.yml: typecheck + 90 vitest tests + unsigned electron-builder package on macos-14 and windows-latest.control-plane.ymlgains tray-darwin (macos-14go build+go vetofcmd/af-tray) — previously the darwin CGO tray was only compiled at release time.Known gaps / notes
af server(the don't-fight-the-port guard is what got exercised instead), the "unhealthy control plane" visual state, and the gold/active tray glyph on a live control plane. Logic is unit-tested; these fall out of any run against a free :8080.agentfield-useskill (using installed agents) is a queued follow-up.macOS smoke checklist
open agentfield://agents,open agentfield://settings— window focuses and switches view.af servercomes up (port 8080 free), tray dot on the af-tray side goes active.cmd/af-tray, run it, click "Open Dashboard" — should open the desktop app (not the browser); delete the app and it should fall back to the web UI.🤖 Generated with Claude Code