Skip to content

Add Agent Island for Active Agents - #756

Merged
onevcat merged 34 commits into
mainfrom
feat/agent-island
Sep 3, 2026
Merged

Add Agent Island for Active Agents#756
onevcat merged 34 commits into
mainfrom
feat/agent-island

Conversation

@onevcat

@onevcat onevcat commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Continues #753 from @SunChJ on a fork-owned branch. This branch contains every commit from the original PR, so merging it with a merge commit also marks #753 as merged and keeps the author's history and attribution intact.

Agent Island is an opt-in, notch-aware projection of the Active Agents roster at the top of a selected display. It adds no agent state or acknowledgement model: Working stays a compact carousel, Blocked and unviewed Done entries surface as a compact attention collection, and the expanded roster composes the same row presentation and context menu as the sidebar. See docs/components/agent-island.md and docs-ai/066-agent-island/ for the design record.

Changes on top of #753

  • Reset isIslandHovered when the roster empties. Hovering pauses the carousel timer; when the last entry left, the compact island unmounted without a hover-exit, so the stale flag kept later Working pairs from rotating. Covered by a TestClock regression. Closes the remaining blocker from the Add Agent Island for Active Agents #753 review.
  • Match the Display picker by display UUID. Tags compared the full .display(id:name:) value, but name comes from NSScreen.localizedName and changes with the system language or a rename, leaving the picker with no matching tag. AgentIslandDisplaySelection is an id-only picker identity mapped back to the stored preference with the catalog's current name. Covered by unit tests.
  • Revert unrelated line wrapping. The island commits were formatted by a bare swift-format run, which does not discover .swift-format.json and defaults to 100 columns. AppFeature.swift carried 78 lines of pure reflow next to 12 lines of real change; every whitespace-only hunk is restored so hot files diff by their functional changes alone.
  • Align the compact bar with the notch. The notched bar was max(40, cutout + 8)pt tall and hung 8pt below the menu bar; it now uses the measured cutout height (which already follows the display scaling mode) with a 28pt floor for the icon cluster.
  • Tune the notched bar. 12pt bottom corners and 20pt icons in a 24pt cluster; the floating pill keeps 21pt.
  • Stop the icon cluster from springing back after panel resizes. The hosting view centers content vertically and the panel is resized only after layout, so when an attention cell appeared or disappeared the cluster (the one subtree with an .animation keyed on every entry refresh) interpolated from the transient offset and looked like it vanished and dropped in from above. The root is now pinned to the top and the cluster animation is keyed on membership/order only. The floating pill also no longer tears the cluster down on each carousel rotation.
  • Fold island actions into one wrapper. island(Action) replaces three parallel cases and a context-menu router; the reducer forwards the wrapped sidebar action and collapses only when it presents Prowl UI, AppFeature surfaces the window in one place. Island navigation tests move out of the quit test file.
  • Create the panel only while the setting is on. The controller observes agentIslandEnabled and starts or stops the panel, so a disabled island costs nothing at launch.
  • Tidy structure. The live display catalog moves from Models/ to BusinessLogic/; the unused rootWidth is gone.
  • Drop clipped shadows and match the roster width. The panel is content-sized so every shadow was cut at the window edge; the roster now takes the bar width (425pt on a 14-inch notch instead of a fixed 420pt).
  • Per-state counts instead of a name carousel. Both the notched wing and the floating pill show blocked / done / working / idle counts as state-colored SF Symbols (omitting empty states) instead of "N Agents" or a rotating Working name. With no consumer left, the carousel reducer state, four-second clock effect, hover tracking, and their tests are removed.
  • Roster rows show "pane title · branch". The island roster has the width for both, so it no longer follows the sidebar's either/or setting; a live Workflow badge still takes the line.
  • Highlight the roster from the terminal manager's focus. The island dimmed rows against the reducer's focusedSurfaceID, a keyboard anchor fed by per-worktree deduplicated focus events that goes stale when a worktree is re-selected; it now uses the same source as the sidebar overlay. The display catalog is resolved on first use so a disabled island installs nothing at launch, and the lifecycle test now goes through the real observation instead of calling refreshLifecycle() itself. (Adversarial review, 2026-09-03.)
  • Consolidate the design record. The nine working-note amendments under docs-ai/066-agent-island/ (four marked superseded by the next) are folded into 000-plan.md and 001-action.md; docs/components/agent-island.md now describes current behavior only.

Known limitation

On a notched display the bar sits on top of the menu bar, so while agents are running the ~120pt wing on each side of the cutout covers that part of the menu bar band; a menu title or status item under a wing is not clickable until the island disappears. Documented in docs/components/agent-island.md; narrowing the wings or offering floating placement on notched displays is an open product decision.

Testing

  • ActiveAgentsFeatureTests, AgentIslandScreenTests, SettingsFeatureTests, SettingsFilePersistenceTests, BaguaWorkingIndicatorTests, AppFeatureQuitTests, AppFeatureSettingsChangedTests, AgentIslandIsolationTests, AppFeatureAgentIslandTests, AgentIslandStateSummaryTests, AgentIslandRosterContentTests, AgentIslandIconClusterTests: 152 tests pass on the latest head; make build-app zero warnings
  • make check passes
  • CI on the original head (c4606b06) was green

https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL

SunChJ and others added 30 commits September 1, 2026 11:25
Hovering the compact island pauses the carousel timer. When the last
entry is removed the compact view unmounts, so SwiftUI never delivers
the trailing hover-exit and `isIslandHovered` stayed true. Any later
pair of Working agents then saw the stale flag and never rotated.

Clear the hover flag alongside the roster-expanded flag whenever the
entries become empty, and add a TestClock regression for hover ->
remove last entry -> two Working entries -> tick at four seconds.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The island commits were formatted by a bare `swift-format` run, which
does not discover `.swift-format.json` and falls back to the default
100-column limit. Because the project config sets
`respectsExistingLineBreaks`, lint accepted the narrower wrapping, so
`AppFeature.swift` carried 78 lines of pure reflow next to 12 lines of
real change. Restore every hunk whose only difference is whitespace so
the hot files diff against main by their functional changes alone.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The picker tagged rows with the full `.display(id:name:)` value, but
`name` comes from `NSScreen.localizedName`, which changes with the
system language or a display rename. A stored preference then matched
no tag, so the picker showed an empty selection and logged an invalid
selection warning even though placement still resolved by id.

Introduce `AgentIslandDisplaySelection`, an id-only picker identity,
and map it back to the stored preference using the connected catalog's
current name, keeping the last-known name for a disconnected display.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
`docs-ai/066-agent-island/` had grown into a working-note archive: nine
amendment files, four of them marked superseded by the next, recording
design directions that were tried and dropped inside a single PR. Fold
them into the plan (rejected directions under Alternatives & decisions)
and the action log (one timeline row per wave), and rewrite the user
manual page to describe current behavior without the iteration history.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The notched compact bar was `max(40, cutout + 8)` points tall, so on a
14-inch display with a 32pt inset it hung 8pt below the menu bar into
the application area. Use the measured cutout height directly, which
already tracks the display's scaling mode, keeping only a 28pt floor
so the 27pt icon cluster never clips on a short inset.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
With the compact bar now only as tall as the cutout, the 17pt bottom
corners read rounder than the notch itself and the 21pt icons sat
2.5pt from the edge. Use a 12pt radius and, in the notched layout
only, 20pt icons in a 24pt cluster; the floating pill keeps 21pt.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The panel is resized to fit after SwiftUI lays out new content, and the
hosting view centers content vertically, so for one pass a taller or
shorter island sat offset from the top edge. The icon cluster keyed its
swap animation on the full entry values, which change on every refresh,
so that offset was interpolated with the spring: the cluster appeared
to vanish and drop back in from above whenever an attention cell came
or went. Pin the root to the top so height changes never move the bar,
and key the cluster animation on membership and order only.

The floating pill also wrapped the cluster in the carousel's per-entry
identity, so each rotation removed and re-inserted the icons through
the text transition. Scope the identity to the text alone.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
Three parallel cases (islandEntryTapped, islandHandOffTapped,
islandRunWorkflowTapped) plus a context-menu router all expressed the
same rule: collapse the roster, surface Prowl, then run the sidebar
action. Replace them with `island(Action)`. The reducer forwards the
wrapped action and collapses only when it presents Prowl UI
(`surfacesProwl`); AppFeature surfaces the window in one case instead
of four, and Open Prowl now calls the lifecycle client synchronously
like its siblings. The island navigation tests move out of the quit
test file into their own.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The controller created its panel, hosting view, and screen observers at
launch for every user, including the default-off majority. Observe the
enabled flag on the store and start or stop the panel as it changes;
`refreshLifecycle()` is the synchronous core the observer and the test
share.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
AgentIslandDisplayCatalog observes screen-parameter notifications and
owns the live NSScreen mapping, so it belongs with the controller in
BusinessLogic rather than beside the pure geometry in Models. Drop the
unused AgentIslandNotchLayout.rootWidth.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The panel is sized to its content, so every shadow was clipped at the
window edge. Remove them from the compact bar, the attention
collection, and the roster. The roster also used a fixed 420pt while
the notched bar is 425pt on a 14-inch display, leaving a 2.5pt step on
each side; it now takes the root width.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The leading wing showed "N Agents" or one carousel name, neither of
which says what needs attention. Replace it with counts per state in
attention order (blocked, done, working, idle), each as a state-colored
SF Symbol plus a number, omitting empty states so the 108pt wing only
spends width on what exists. The symbols follow the Shelf spine markers
with an added Idle glyph. The floating pill keeps its name carousel.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The floating pill now shows the same per-state counts as the notched
wing, one size up, so both placements read the same way. That leaves
the Working-name carousel with no consumer: remove its reducer state
(current entry, hover flag), the four-second clock effect, the hover
action, and the derived Working projections, along with their tests.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The sidebar shows one or the other because it is narrow; the island
roster has room for both, so its subtitle is now "pane title · branch",
skipping the repeat when they match. A live Workflow badge still takes
the whole line, as in the sidebar.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The controller's default argument evaluated AgentIslandDisplayCatalog
.shared at construction, so every launch enumerated displays and
installed a screen-parameter observer even with the island off. Keep
the injection point for tests but resolve the shared catalog lazily.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The roster dimmed rows against the reducer's focusedSurfaceID, which
is a keyboard-navigation anchor fed by per-worktree deduplicated
focusChanged events: select worktree A, then B, then A again and the
anchor still says B, so the island highlighted the wrong pane. Use
the same source as the sidebar overlay, the terminal manager's active
surface for the selected worktree.

The lifecycle test now drives the setting through the store and waits
for the observation to react instead of calling refreshLifecycle()
itself, so a broken re-registration fails it.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
The roster (19pt) and attention collection (14pt) now use the same
12pt corners as the notched bar. Runtime icons keep their glyph size
but the ring circle grows by one point of radius so the glyph and the
rotating ring no longer touch; the cluster grows with it.

Claude-Session: https://claude.ai/code/session_01PuwHwiuyozSN88w7EgTPAL
@onevcat
onevcat merged commit 9eaff98 into main Sep 3, 2026
1 check passed
@onevcat
onevcat deleted the feat/agent-island branch September 3, 2026 01:52
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.

2 participants