Skip to content

[C] Task 1: Notch Bar — adaptive shape, synthetic notch, hover affordance#2

Open
sheepxux wants to merge 60 commits into
mainfrom
c/notch-panel
Open

[C] Task 1: Notch Bar — adaptive shape, synthetic notch, hover affordance#2
sheepxux wants to merge 60 commits into
mainfrom
c/notch-panel

Conversation

@sheepxux

@sheepxux sheepxux commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Summary

Task 1 complete: collapsed Notch Bar with full design polish.

Visual

  • Adaptive layout: notched displays use the real notch dimensions; no-notch displays render a synthetic black notch flush with the screen top.
  • Continuous-curvature ("squircle") rounded bottom corners.
  • Notched bar protrudes 7pt below the hardware notch (Dynamic Island feel).
  • Synthetic notch fills the OS menu-bar height exactly (uses frame.maxY - visibleFrame.maxY for accurate measurement).

Hover affordance

  • Width grows by 50pt (capsule) / 30pt (notched) with spring animation.
  • Capsule drops 8pt below the menu bar on hover.
  • Status dot + count fade in on hover (idle = clean silhouette).
  • Pointer cursor changes to .pointingHand.
  • System NSWindow shadow toggles on for hover (Apple-native, no compositor seams).

Adaptivity

  • Re-measures on didChangeScreenParametersNotification — display swap, "Larger Text", scale changes are all auto-handled.
  • Notched ⇄ synthetic-notch flips automatically when window moves to a different display.

Architecture

  • Split into IslandApp (exec, @main + AppDelegate) + IslandAppLib (library, all view code) so SwiftUI Previews work under Xcode 26's ENABLE_DEBUG_DYLIB requirement.
  • All UI consumes TaskStore from IslandCore per the contract.

Test plan

  • Open in Xcode 26+ on macOS 14+
  • Switch scheme to IslandApp → ⌘R
  • Confirm: bar appears at top of screen, vertically centered in menu bar
  • Hover: bar widens, content fades in, shadow appears, cursor changes
  • Leave hover: bar shrinks back, shadow + content disappear
  • Switch scheme to IslandAppLib → open NotchBarView.swift → Canvas Resume
  • Confirm: 5-state previews render correctly for both notched and capsule modes

Summary by cubic

Builds the collapsed Notch Bar with adaptive/synthetic notch, hover affordance, and a single‑window bar→panel morph backed by live IslandCore data. Adds Settings, banner notifications, and a release pipeline; rebrands the app to “Dev Island” with a new icon, updated bundle/Cask ids, and a bundle filename fix so Finder shows “Dev Island” everywhere.

  • New Features

    • Adaptive bar: real‑notch cutout; no‑notch screens get a flat‑top synthetic notch; auto‑repositions on screen changes.
    • Hover affordance: widens, pointer cursor, native NSWindow shadow.
    • Panel: click bar to expand; hover‑out/Esc/click‑outside collapses; task list with “Connect Service” footer.
    • Data + notifications: live Manus tasks via ManusAPIClient with Hummingbird webhook + tunnel and 60s polling fallback; macOS banners for running→completed and any→waiting; taps open task URLs; enabled only in bundled .app builds.
    • Settings: Manus API key connect/disconnect (Keychain + live validate), Launch at Login via SMAppService, Quit; gear opens it; API key placeholder updated to sk‑….
    • Single window + safety + polish: one borderless IslandWindow morphs a shared NotchPanelShape; transparent areas click‑through via cursor‑tracked ignoresMouseEvents; 250ms synced color transitions; bar dims to idle gray when .disconnected/.reconnecting (.degraded keeps live colors).
  • Release & Build

    • Branding: app name “Dev Island”; bundle id app.devisland.Island; new AppIcon; Homebrew Cask renamed to dev-island; VERSION bumped to 0.1.1; Keychain/UserDefaults namespaces updated (pre‑0.1.1 installs may need to re‑enter the API key); bundle filename now “Dev Island.app” and used across scripts/workflow/Cask so Finder shows the correct name everywhere.
    • Release workflow: tag‑triggered GitHub Actions builds → codesigns → notarizes → staples → uploads both Dev-Island.zip and Dev-Island-<VERSION>.zip; prints sha256 for the Cask.
    • SPM → .app: scripts/build-app.sh wraps the executable; App icon bundled; Info.plist added; build output ignored in build/.
    • Homebrew Cask draft under dist/homebrew-island; Cask declares depends_on cask: "cloudflared".
    • cloudflared resolution at runtime: /opt/homebrew/bin/usr/local/bin$PATH; falls back to 60s polling if missing.
    • CI hardening: macOS 15 (Swift 6), correct .p12 import flags, longer notarize timeouts, contents: write permissions; previews guarded with #if PREVIEWS && DEBUG.

Written for commit a635299. Summary will update on new commits.

Axu and others added 18 commits April 25, 2026 00:32
- Theme tokens: Palette, Motion, Typo, NotchMetrics
- NotchBarShape: dual-rect path framing the hardware notch
- StatusDot: static color per BarState (animations come in Task 3)
- NotchBarView: bar composition with status dot + task count
- NotchBarWindow: borderless .statusBar level, multi-Space + full-screen aux
- AppDelegate: spawns the bar after launch, repositions on screen change

Build gating: #Preview blocks wrapped in #if PREVIEWS so swift build still
passes on Command Line Tools-only machines. Xcode auto-defines PREVIEWS via
Package.swift detection of /Applications/Xcode.app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…Previews

Xcode 26 requires ENABLE_DEBUG_DYLIB=YES for previewing in executable
targets, but SwiftPM does not expose that build setting. The recommended
workaround (per the Preview error message) is to move preview-able code
into a library target.

- IslandApp executable target now contains only the @main entry + AppDelegate
- All Theme/Views/Windows code moves to new IslandAppLib library target
- NotchBarWindow + init + reposition() promoted to public so the exec can
  reach them from AppDelegate
- Files relocated with git mv to preserve history

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: status-bar/notch height varies per Mac, and the bar should
have a rounded "tab" silhouette instead of sharp rectangles.

- NotchMetrics.Layout: per-screen snapshot driving Window framing + Shape
  drawing. Reads NSScreen.safeAreaInsets.top on notched displays and
  NSStatusBar.system.thickness on plain displays.
- NotchBarShape: rounded outer-bottom corners on each side extension
  (cornerRadius 14pt). Inner edges stay sharp; the S-curve transition will
  arrive with the panel in Task 2.
- NotchBarView: dual-mode rendering — NotchBarShape on notched screens,
  Capsule on no-notch (Mac mini, external displays); status dot + count
  laid out appropriately for each.
- NotchBarWindow: re-measures on every reposition() so screen-parameter
  changes (display swap, scale change) flip notched ⇄ capsule + adjust
  height without a relaunch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace hand-drawn quarter-circle arcs with UnevenRoundedRectangle in
.continuous style (Apple's squircle math, same as macOS app icons). The
curvature transition is no longer abrupt at the corner endpoints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: side vertical edges should read taller, with shorter
curve transitions (Dynamic Island feel).

- Bar now protrudes bottomOverhang (7pt) below the hardware notch on
  notched displays, giving the side a visible straight stretch before
  the curve starts.
- NotchBarShape rewritten as one connected outline with a top-center
  cutout at notchHeight × notchWidth. The bottom corners use the same
  continuous-curvature squircle.
- cornerRadius dropped 14 → 11 so the curve occupies less of the bottom.
- NotchMetrics.Layout adds notchHeight separate from barHeight to drive
  the cutout sizing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hover the bar / capsule → it grows wider + slightly taller with a spring
animation, and the cursor switches to pointingHand. Signals "clickable"
without firing the panel expansion (that's still Task 2).

- NotchMetrics adds hover-boost constants (notched: +30w +4h, capsule:
  +50w +6h) and Layout.hovered() returning the expanded snapshot.
- NotchBarWindow sizes itself to the hover-expanded dimensions so the
  bar can animate inside the window without any NSWindow frame churn.
- NotchBarRootView holds @State isHovering, animates between idle and
  hover layouts via withAnimation(.spring(response: 0.3, damping: 0.75)).
- Hover hit-testing uses contentShape(barHitShape) so the cursor only
  reacts over the actual bar pixels, not transparent window padding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: hover-expanded capsule overflowed the menu-bar zone.

- Idle capsule height = thickness - capsuleVerticalInset (6pt). Sits
  inside the OS status bar with a small visible gap top + bottom.
- Hover capsule height = thickness (full status bar). No overflow.
- Capsule alignment in window switched to .center so growth is symmetric
  (top + bottom expand toward the status-bar edges). Notched bar still
  anchors .top so it grows downward into the menu-bar zone (Dynamic
  Island feel preserved).
- Removed fallbackTopMargin constant; topMargin is now always 0 because
  the OS status bar zone is centered around the capsule via SwiftUI
  alignment instead of pixel offsets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: capsule sat too high in the menu bar zone, and felt small.

Root cause: NSStatusBar.system.thickness (~22pt) under-reports the actual
visible menu-bar height on macOS 26 (~28pt). Sizing the window to thickness
made the window land at the top of the screen but cover only the upper
slice of the menu bar — the centered capsule then drifted upward.

- NotchMetrics.Layout gains menuBarHeight, measured from the screen as
  `frame.maxY - visibleFrame.maxY` (authoritative regardless of OS
  changes). Falls back to thickness when visibleFrame is unavailable.
- Window container height (no-notch) = menuBarHeight, so the window
  covers the status-bar zone exactly and the centered capsule sits at
  its true vertical center.
- Hover capsule height = menuBarHeight (still fills the bar exactly).
- Idle capsule width 140 → 168, height inset trimmed 6 → 4 — both
  cooperate to give the pill more presence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: prefer a MacBook-style notch silhouette over a centered
capsule, with the bar flush against the screen edge (no top gap).

- No-notch backdrop switched from Capsule (#0A0A0A, all-rounded) to
  UnevenRoundedRectangle in .continuous style (#000000, flat top +
  rounded bottom corners) — visually a "fake notch" that hangs from
  the screen edge identical to a real one.
- Window container anchors the bar to TOP for both modes; the no-notch
  container height equals the OS menu-bar height so the bar hangs from
  the screen edge and grows downward into the menu bar zone on hover.
- Hover hit-test shape mirrors the new fake-notch silhouette.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: synthetic notch should fill the status bar height at idle;
hover should only widen it, not change height.

- Removed capsuleVerticalInset; barHeight now == menuBarHeight at idle.
- Layout.hovered() already sets barHeight = menuBarHeight on no-notch,
  so the width-only growth pattern falls out for free.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback (no-notch displays only):
- Idle should show a clean fake-notch silhouette without status dot or count.
- Hover should drop slightly past the menu bar bottom (Dynamic Island feel).

- NotchBarView gains showsContent flag; opacity-fades the dot + count.
  Notched mode keeps showsContent always true (the bar is always visible
  around the hardware notch); synthetic-notch mode toggles on hover.
- NotchMetrics adds hoverHeightBoostCapsule (8pt). Layout.hovered() for
  no-notch now returns barHeight = menuBarHeight + boost.
- Window container sized to hover dimensions so the bar can expand into
  the extra height without changing the NSWindow frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Soft black shadow appears under the bar on hover, animating in with the
size growth and fading out on exit. Window container gains shadowPadding
(28pt sides + bottom) so the shadow renders without NSWindow clipping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous shadow (opacity 0.45, radius 16, y 8) produced a visible hard
edge between the bar bottom and its shadow. Replaced with two stacked
soft shadows — a close tight one (0.18 / radius 6) for definition and
a wide ambient one (0.12 / radius 22) for diffusion. Lower offsets so
the shadow sits around the bar instead of below.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the two-layer shadow with one very soft ambient one (opacity 0.10,
radius 20, y=0). Add .compositingGroup() before the shadow so the bar's
flat edges don't produce a hard shadow band.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The .shadow primitive in SwiftUI was producing a faint horizontal seam
where the bar's flat bottom met its shadow, even with .compositingGroup
and zero offset. The seam was a compositor artifact tied to the way
shadow gets sampled along sharp shape edges.

Switch to a manual halo: render the bar's silhouette behind the bar,
filled black, scaled 1.06×1.20 and blurred radius 18. The halo's
center is fully covered by the actual bar; only the soft blurred edges
peek out as a glow. Because it's a real blurred shape (not a shadow
filter), the falloff is smooth gaussian, no artifacts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the manual halo and SwiftUI .shadow attempts entirely. NSWindow
with hasShadow=true and isOpaque=false renders the system's standard
window shadow against the bar's alpha silhouette — same model used by
Finder, Dock, and Notification Center. Subtle, clean, no compositor
seams.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire SwiftUI hover state back to NSWindow.hasShadow via an onHoverChange
closure. Idle state: hasShadow=false (no shadow, clean silhouette).
Hover: hasShadow=true + invalidateShadow() so the system recomputes
against the bar's alpha mask. Same Apple-native shadow as Finder, only
revealed on hover.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First stage of Task 2 (Notch Panel). Pure SwiftUI rendering with mock
data; PanelWindow + hover-trigger logic land in Stage B.

- NotchPanelShape: continuous-curvature rounded-bottom outline with
  optional top-center cutout for the hardware notch on notched displays.
  Falls back to a clean rounded-bottom rect on synthetic-notch / no-notch
  displays.
- TaskCard: 3pt state-color stripe + tool logo + title + phase·duration +
  chevron. Running tasks get a 2pt animated progress shimmer at the
  bottom. Hover swaps cardBg → cardHover. Live duration via 1Hz timer.
- NotchPanelView: header (Tasks (N) + connection dot + ⚙) → scrollable
  task list (or empty state) → divider → "Connect Service" footer.
  Reserves notchHeight at top on notched displays so content doesn't
  fight the hardware notch.
- NotchMetrics gains panelCornerRadius (22), panelWidth (380), and
  panelMaxWidth (480).

Previews available in Xcode for: shape (notched + plain), TaskCard
all states, NotchPanelView (no-notch / notched / empty).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="IslandAppLib/Windows/NotchBarWindow.swift">

<violation number="1" location="IslandAppLib/Windows/NotchBarWindow.swift:55">
P2: `reposition()` uses `NSScreen.main`, which can compute notch metrics/placement from the wrong monitor. Use the window’s current screen first when recalculating layout.</violation>
</file>

<file name="Package.swift">

<violation number="1" location="Package.swift:9">
P2: Avoid hardcoding `/Applications/Xcode.app` to detect Xcode builds; this is path-fragile and can disable `PREVIEWS` in valid Xcode environments.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread IslandAppLib/Windows/NotchBarWindow.swift Outdated
Comment thread Package.swift
Axu and others added 5 commits April 26, 2026 00:05
…splays

User feedback: the empty space flanking the hardware notch is wasted.

- On notched displays, the title "Tasks (N)" now lives in the LEFT side
  extension and the connection dot + ⚙ in the RIGHT side extension, both
  vertically centered against the notch.
- The notch corridor in the middle stays transparent for the hardware
  notch to fill.
- No-notch panels keep the conventional top header (no side-extension
  concept on a single rounded-bottom shape).
- Removed the now-unused notchReserve spacer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ator

Wire bar ⇄ panel transitions.

- IslandCoordinator (@mainactor): single shared orchestrator owning hover
  dwell timers (expand 120ms / collapse 300ms) and the current Mode
  (.collapsed | .expanded). Public callback `onModeChange` is the bridge
  to window visibility.
- PanelWindow: borderless statusBar-level NSWindow hosting NotchPanelView,
  sized to its SwiftUI fittingSize so it grows/shrinks with the task
  list. Top edge flush with screen top, centered horizontally.
- NotchPanelRootView: subscribes to TaskStore.shared, calls into
  Coordinator on hover IN/OUT, and dispatches taps to
  store.openTaskInBrowser.
- NotchBarRootView.handleHover: schedules expand on hover IN, cancels on
  hover OUT.
- AppDelegate creates both windows, sets up the coordinator->visibility
  bridge, and re-pins both on screen-parameter changes.

Esc / click-outside dismiss + crossfade animation come in Stage B.2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
API layer
- ManusAPIClient: GET /v1/tasks, POST|DELETE /v1/webhooks, GET /v1/tasks/{id}
- Auth header: API_KEY (verified with real key)
- Real API shape: data[] top-level, Unix-timestamp-string dates, metadata.task_title/url
- 429 exponential backoff: 30s → 90s → 300s per spec §8
- ManusError.networkUnavailable for URLError detection

Webhook & tunnel
- WebhookServer (Hummingbird 2.x, port 7823): POST /webhook + RSA-SHA256 sig verify
- CloudflaredProcess: stderr URL parsing, 30s timeout
- TunnelManager: 5-min/3-restart limit, suspend (sleep) vs stop, heartbeat

State & storage
- TaskStore (@mainactor @observable): bootstrap, sleep/wake, degraded-mode fallback
- StateReconciler: pure reconcile() + apply() for polling and webhook events
- PollingFallback: 60s Task.sleep loop, onNetworkError/onNetworkRestored callbacks
- KeychainStore: SecItem upsert, kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- SQLiteStore (actor): tasks + progress_events, write-only v1

Tests
- ManusAPIClientTests (MockURLProtocol): 401/429/200/networkUnavailable
- WebhookPayloadTests: all 3 event types + unknown-event failure
- StateReconcilerTests: reconcile conflicts, webhook apply
- KeychainStoreTests: save/load/overwrite/delete

Docs
- docs/INTERFACE_CONTRACT.md: TaskStore v1 public API
- docs/manus-api-field-notes.md: real API field mapping from live testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the two-window architecture (NotchBarWindow + PanelWindow with
cross-fade visibility) with a single IslandWindow whose SwiftUI root
(IslandRootView) morphs the NotchPanelShape in place between bar and
panel modes. Eliminates the cross-fade seam between two NSWindows by
making the bar↔panel transition a single shape morph driven by SwiftUI
withAnimation.

Architecture
- IslandWindow sized to the LARGEST visible state (panelMaxWidth ×
  expandedHeight + shadowPadding); position is fixed at screen top, only
  the inner SwiftUI shape morphs.
- IslandRootView owns the morph: shapeWidth / shapeHeight / cornerRadius
  / opacity all derive from `IslandCoordinator.mode` and a local
  isHovering @State; mode flips inside withAnimation(modeAnimation) so
  every dependent value interpolates in lockstep.
- NotchBarView and NotchPanelView gain a `drawsBackdrop: Bool` flag — when
  hosted by IslandRootView they render content only, with the shared
  silhouette drawn once by the root.
- IslandCoordinator becomes @observable; mode changes wrap in
  modeAnimation. AppDelegate's onModeChange handler now only manages
  Esc/click-outside event monitors; visibility toggling is gone.

Animation polish
- NotchPanelShape gains animatableData over (cornerRadius, notchWidth,
  notchHeight) so cornerRadius interpolates with the morphing frame
  instead of snapping at t=0+ to the new value (which produced an
  over-rounded silhouette during the first few frames of the morph).
- IslandRootView.handleTap hands isHovering off to modeAnimation in the
  same withAnimation transaction as coordinator.expand(), so a stale
  hover-spring (response 0.3) doesn't fight the mode-spring (response
  0.42) for ownership of shapeWidth/Height.
- modeAnimation switches from .spring(response: 0.42, dampingFraction:
  0.78) to .smooth(duration: 0.42, extraBounce: 0.0). The morph spans
  ~7× in height (43→360pt); spring overshoot at that scale reads as
  panel wobble, not playfulness. .smooth gives critically-damped motion
  with a continuous velocity profile (no abrupt acceleration at t=0),
  which keeps the silhouette edge crisp through the first frames where
  sub-pixel reflow would otherwise show as a seam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a DEBUG-only sandbox window that drives the island without Manus,
so every dev launch has live controls for verifying the bar / panel /
state animations on real hardware (Previews aren't enough — they don't
exercise the live IslandWindow / IslandCoordinator).

Surface
- DebugSandboxWindow: titled, floating NSWindow parked bottom-right of
  the main screen.
- DebugSandboxView: three control rows
    Coordinator  — Expand / Collapse / Toggle / Reposition
    Tasks        — + Running / + Waiting / + Completed / + Failed
                   Seed 3 (preview set) / Clear all
    Connection   — Connected / Reconnecting / Disconnected
  plus a live-state line (mode / task count / connection) at the bottom.
- TaskStore.swift: extends the existing #if DEBUG extension with
  debugSetTasks / debugAppend / debugClearTasks / debugSetConnectionStatus
  / debugSpawn. Same-file extension is required because tasks /
  connectionStatus are private(set); this keeps the production setter
  contract untouched and scopes the debug write surface to one #if DEBUG
  block.
- AppDelegate: opens the sandbox at applicationDidFinishLaunching inside
  a single #if DEBUG block.

Boundaries
- All sandbox code is physically scoped to IslandAppLib/Debug/ and the
  TaskStore #if DEBUG extension. Every file is wrapped in #if DEBUG, so
  release builds neither compile nor link any of this.
- Naming: every mutator is prefixed `debug…` so call sites read like
  sandbox plumbing, not domain logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 13 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="IslandAppLib/Views/NotchBar/StatusDot.swift">

<violation number="1" location="IslandAppLib/Views/NotchBar/StatusDot.swift:90">
P2: Guard the first delayed completed-flash callback against stale runs; otherwise a previous flash can cancel a newer flash during quick state changes.</violation>
</file>

<file name="IslandAppLib/Views/Island/IslandRootView.swift">

<violation number="1" location="IslandAppLib/Views/Island/IslandRootView.swift:238">
P2: Click-to-expand can leave a pushed pointing-hand cursor unbalanced because expansion bypasses the collapsed hover-exit pop path.</violation>
</file>

<file name="IslandApp/IslandApp.swift">

<violation number="1" location="IslandApp/IslandApp.swift:99">
P2: Escape handling is wired only to a global key monitor, so Esc collapse is unreliable. Add a local key monitor (and optionally keep global) so Esc works when this app is active and doesn’t depend solely on accessibility-granted global key capture.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +90 to +98
DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
withAnimation(.easeIn(duration: 0.27)) {
completedScale = 1.0
}
// Allow the timeline to pause after the flash completes.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.30) {
if lastFlashedAt == flashStart { lastFlashedAt = nil }
}
}

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard the first delayed completed-flash callback against stale runs; otherwise a previous flash can cancel a newer flash during quick state changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At IslandAppLib/Views/NotchBar/StatusDot.swift, line 90:

<comment>Guard the first delayed completed-flash callback against stale runs; otherwise a previous flash can cancel a newer flash during quick state changes.</comment>

<file context>
@@ -1,34 +1,234 @@
+            }
+            let flashStart = Date()
+            lastFlashedAt = flashStart
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
+                withAnimation(.easeIn(duration: 0.27)) {
+                    completedScale = 1.0
</file context>
Suggested change
DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
withAnimation(.easeIn(duration: 0.27)) {
completedScale = 1.0
}
// Allow the timeline to pause after the flash completes.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.30) {
if lastFlashedAt == flashStart { lastFlashedAt = nil }
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
guard lastFlashedAt == flashStart else { return }
withAnimation(.easeIn(duration: 0.27)) {
completedScale = 1.0
}
// Allow the timeline to pause after the flash completes.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.30) {
if lastFlashedAt == flashStart { lastFlashedAt = nil }
}
}
Fix with Cubic

Comment thread IslandAppLib/Views/Island/IslandRootView.swift
Comment thread IslandApp/IslandApp.swift
Comment on lines +99 to +106
if let escMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: { event in
// 53 = Escape (kVK_Escape).
if event.keyCode == 53 {
Task { @MainActor in IslandCoordinator.shared.collapse() }
}
}) {
panelEventMonitors.append(escMonitor)
}

@cubic-dev-ai cubic-dev-ai Bot Apr 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Escape handling is wired only to a global key monitor, so Esc collapse is unreliable. Add a local key monitor (and optionally keep global) so Esc works when this app is active and doesn’t depend solely on accessibility-granted global key capture.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At IslandApp/IslandApp.swift, line 99:

<comment>Escape handling is wired only to a global key monitor, so Esc collapse is unreliable. Add a local key monitor (and optionally keep global) so Esc works when this app is active and doesn’t depend solely on accessibility-granted global key capture.</comment>

<file context>
@@ -16,57 +16,109 @@ struct IslandApp: App {
+    private func installPanelEventMonitors() {
+        guard panelEventMonitors.isEmpty else { return }
+
+        if let escMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: { event in
+            // 53 = Escape (kVK_Escape).
+            if event.keyCode == 53 {
</file context>
Suggested change
if let escMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: { event in
// 53 = Escape (kVK_Escape).
if event.keyCode == 53 {
Task { @MainActor in IslandCoordinator.shared.collapse() }
}
}) {
panelEventMonitors.append(escMonitor)
}
if let escLocalMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: { event in
// 53 = Escape (kVK_Escape).
if event.keyCode == 53 {
Task { @MainActor in IslandCoordinator.shared.collapse() }
return nil
}
return event
}) {
panelEventMonitors.append(escLocalMonitor)
}
if let escGlobalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: { event in
if event.keyCode == 53 {
Task { @MainActor in IslandCoordinator.shared.collapse() }
}
}) {
panelEventMonitors.append(escGlobalMonitor)
}
Fix with Cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="IslandAppLib/Views/NotchBar/NotchBarView.swift">

<violation number="1" location="IslandAppLib/Views/NotchBar/NotchBarView.swift:82">
P3: Remove the temporary DEBUG diagnostic background tint from `NotchBarView`; it changes the component’s visual output in debug/previews and can skew UI verification.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread IslandAppLib/Views/NotchBar/NotchBarView.swift Outdated
@cubic-dev-ai

cubic-dev-ai Bot commented Apr 28, 2026

Copy link
Copy Markdown

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Three coordinated changes to make the bar's status readout reliably
visible at idle on every display, including non-notched setups (Mac
mini + external monitor) where the previous 'clean silhouette at idle'
design hid content until hover:

- NotchBarView: drop the side-extension split layout for notched
  displays. The dot + count now sit in a single horizontal row in the
  black shelf BELOW the hardware notch, where pixels are actually
  drawable and the menu-bar's status items don't compete for the
  same screen area. Synthetic-notch path is unchanged.
- NotchMetrics: bottomOverhang 7 → 24, so the shelf below the notch
  has room for the readout (was sized only for a Dynamic-Island-style
  visual lip).
- IslandRootView: showsContent forced to true, taskCount switched to
  the simple total (store.tasks.count) instead of running+waiting only,
  matching the 'always visible total readout' contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Axu and others added 30 commits April 28, 2026 05:07
…ring of dot/count

The .frame(maxHeight: .infinity, alignment: .bottom) approach kept
getting overridden by outer frame's default center alignment in
practice, despite the math saying it should work. Switch to a
ZStack(alignment: .bottom) with a Color.clear filler that's
guaranteed to claim the parent's full proposed size — that forces
the alignment .bottom to actually pin the HStack to the bar's
bottom edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…land parity)

Was: `measured + syntheticShelfHeight` (e.g. 24+22=46 or 32+22=54),
which on macOS 26 (with its ~32pt menu bar) left only a ~14-22pt
shelf — too tight for the dot+count to land cleanly below the menu
bar overlay even with bottom anchoring (content kept reading as 'too
high', sitting at the menu-bar boundary).

Switch to a fixed 48pt total — same dimensions VoidChecksum/vibe-
island uses for its always-on-top notch window. 48pt absorbs any
menu-bar height up to ~32pt and still leaves a clean 16pt shelf,
which combined with the existing ZStack(alignment: .bottom)
anchoring inside NotchBarView puts the dot+count solidly in the
drawable area below the menu bar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pt shelf)

User says hardcoded 48pt was 'less appropriate than before'. Restore
the previous formula barHeight = measured menuBarHeight + 22pt shelf.

The actual content-position bug (dot/count appearing near the menu
bar boundary instead of centered in the shelf) is investigated
separately — mucking with bar height was a wrong guess.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ser contract)

User's hard rule: 'Bar 高度 = 菜单栏高度,永远不能改变'. Strip
everything that violated that:

- NotchMetrics: drop `syntheticShelfHeight`. Synthetic-notch
  barHeight equals measured menu-bar height. No shelf below.
- NotchBarView: revert the synthetic-notch content to a plain
  HStack with horizontal padding. No ZStack(alignment: .bottom),
  no Color.clear filler, no anchored-to-shelf gymnastics. Dot +
  count lay out inline, vertically centered by the outer frame.

`showsContent: true` stays in IslandRootView so the dot/count
are not hidden at idle. Whether macOS 26's menu-bar overlay
visibly tints the content is now a separate concern from
positioning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…overlay-based bar rendering

Big restructure to fix the long-running 'dot+count invisible at idle on
synthetic-notch' bug, plus a full visual upgrade for the synthetic-notch
collapsed bar.

Architecture: bar moves out of the ZStack
- Previously bar and panel were sibling children of the shape ZStack.
  NotchPanelView's `fixedSize(vertical: true)` (claiming ~150-395pt
  intrinsic height) was distorting the ZStack's layout proposals to
  the bar — even with explicit ZStack frame and inner alignment, the
  bar's content kept getting positioned wrong.
- Bar is now rendered via `.overlay(alignment: .top)` AFTER the
  ZStack's clipShape. The overlay has its own frame and its own
  clipShape, completely decoupled from panel's layout. Conditional on
  `mode == .collapsed` so it costs nothing during expand. Marked
  `allowsHitTesting(false)` since the parent already routes taps.

Synthetic-notch design: Dynamic Island compact mode
- Three-segment HStack: StatusDot left, current task title middle
  (truncating, fills available width), count badge right with rounded
  white-translucent background.
- Title sourced from highest-priority task's `currentPhase ?? title`,
  using the Waiting > Failed > Running > Completed priority order.
  Falls back to 'No tasks'.
- New typography tokens: `Typo.barTitle` (12pt monospaced regular)
  and `Typo.barBadge` (10pt monospaced semibold).
- New `syntheticCornerRadius = 12` separates the synthetic capsule's
  curve from the hardware-notch wing radius (11). `cornerRadius` in
  IslandRootView now picks per-display.
- `fallbackWidth` 168 → 240 to make room for the title text.
- `fallbackBarHeight = 28` added as a true fallback when the screen's
  measured menu-bar height comes back as 0.

Hardware-notch design: status moves to the shelf below the notch
- Old layout split the dot and count into the side wings flanking
  the hardware notch — visually competed with menu-bar items.
- New layout puts the dot+count in a single horizontal row in the
  visible black shelf BELOW the hardware notch. `bottomOverhang`
  bumped 7 → 24 to host the readout cleanly.

Bug fix: hovered() bar height
- Was `barHeight: menuBarHeight + hoverHeightBoostCapsule` — wiped
  out idle's barHeight and used the menu-bar height as the reference,
  making the hover delta inconsistent with idle.
- Now `barHeight: barHeight + hoverHeightBoostCapsule` — additive
  on top of whatever idle resolved to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… into c/notch-panel

Path A integration: pull S's full IslandCore implementation into our
notch-panel branch so the UI stops driving a stub TaskStore. SwiftUI
views needed zero changes — TaskStore's public API on s/v1-backend
matches the contract our views were already using.

Conflict resolution
- IslandCore/.../TaskStore.swift: keep S's real @mainactor @observable
  implementation as the base; reattach our `#if DEBUG` extension at
  the bottom (mock factory + previewTasks + Sandbox debugSet/Append/
  Clear/SetConnectionStatus/Spawn mutators). The DEBUG extension
  must live in the same file because `tasks` / `connectionStatus`
  are `private(set)` — only same-file extensions can mutate them,
  which is exactly the access boundary we want.
- Package.swift: combine C-side targets (IslandApp, IslandAppLib
  with explicit `path:` and PREVIEWS-aware appSettings) with S-side
  additions (Hummingbird + SQLite deps, IslandCore resources,
  IslandCoreCLI executable, IslandCoreTests target). `appSettings`
  + `coreSettings` declared BEFORE Package(...) so the manifest
  evaluates in order.

S-side compile fixes (S develops on Windows so macOS-only Swift 6.3.1
errors didn't surface for them; tagged each with [C→S] in the source)
- Tunnel/WebhookServer.swift: `import HTTPTypes` to bring in
  `HTTPField` (Hummingbird 2.x doesn't re-export this on Swift 6.3+).
- Manus/ManusAPIClient.swift: explicit `self.consecutive429Count` in
  log-string interpolation closures (Swift 6 default).
- Manus/WebhookSignature.swift: replace `... as Any` with
  `String(describing:)` so the os.Logger interpolator gets a typed
  argument instead of Any.
- Tunnel/TunnelManager.swift: `await` on actor-isolated
  `cloudflaredProcess?.stop()` and `proc.isRunning`. Strict
  concurrency surfaces these on macOS Swift 6.3+.
- Tunnel/{WebhookServer,CloudflaredProcess}.swift: promote actors to
  `public` (with public init/start/stop) so the IslandCoreCLI target
  can use them across module boundaries.
- IslandCoreCLI/main.swift: `await cloudflared.stop()` matches the
  promoted actor signature.

Resource scaffolding
- IslandCore/Sources/IslandCore/Resources/cloudflared: placeholder
  text file so SPM generates `Bundle.module` (which CloudflaredProcess
  reads at runtime). The real cloudflared binary is intentionally
  not committed (size + license). Drop the actual binary at this
  path locally to enable webhook tunneling; without it,
  TunnelManager catches the launch failure and falls back to
  PollingFallback's 60s polling loop.

Verification
- Full clean rebuild succeeds (37s, ~858 modules).
- IslandApp launches and stays alive 4s in smoke test.
- Sandbox window opens (debug mutators still wired against real
  TaskStore.shared since the #if DEBUG extension was preserved).

Open follow-ups for S to consider on s/v1-backend
- Whether WebhookServer / CloudflaredProcess actually want public
  visibility, or the CLI should be refactored to use only the
  public TaskStore surface.
- The `await` and `self.` additions are forward-compatible with
  any Swift version; safe to cherry-pick back to s/v1-backend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + Quit (Task 6)

End-to-end onboarding: panel's gear button now opens a real Settings
window where the user pastes their Manus API key. Hooks straight into
TaskStore.configureAPIKey, so a successful submit flows through to
KeychainStore.save → ManusAPIClient + WebhookServer + PollingFallback
without any further plumbing. The Settings UI rebinds to the same
@observable TaskStore.shared, so the Manus row's status dot reflects
live apiKeyStatus/connectionStatus as they evolve.

Components
- IslandAppLib/Windows/SettingsWindow.swift
  - Titled NSWindow at .normal level, lazily owned by AppDelegate.
  - Defines Notification.Name.islandOpenSettingsRequested as the
    cross-module signal (SwiftUI view → AppDelegate window owner).
  - bringToFront() activates ignoringOtherApps so an .accessory app
    can give the window real key focus.
- IslandAppLib/Views/Settings/SettingsView.swift
  - Connected Services section
      Manus row — live status dot, status line ("Connected" /
        "Reconnecting…" / "Stored key is invalid" / etc), SecureField
        for key entry, async Connect (with ProgressView spinner) /
        Disconnect button. Maps known errors (.unauthorized,
        .networkUnavailable) to friendly copy.
      Coming Soon rows — Claude Code + Cursor, gray dot + Capsule
        badge per spec, dimmed.
  - General section
      Launch at Login toggle wired to SMAppService.mainApp.register()
        / unregister(). Reverts itself if SMAppService throws and
        surfaces the error inline.
  - Footer: Quit Island button (NSApp.terminate, ⌘Q shortcut).
- IslandRootView.handleSettingsTap
  - Now collapses the panel AND posts
    .islandOpenSettingsRequested. Decoupled from window plumbing.
- AppDelegate
  - Adds settingsWindow lazy property + openSettingsObserver that
    listens on .islandOpenSettingsRequested. First call constructs
    the window; subsequent calls bring the existing window forward.
  - Removes the observer in applicationWillTerminate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s through

Bug: IslandWindow is sized to ~536×408 (panel + shadow padding) so the
SwiftUI shape can morph between bar and panel without an NSWindow
frame animation. But when the bar is collapsed, only the small capsule
at top is visible — the rest of that 408pt window is fully transparent
yet still alive. With ignoresMouseEvents=false (the default we need to
make the bar clickable), the entire 408pt window swallows clicks across
the upper-middle of the screen, blocking the user from interacting
with apps under our window's transparent area. Reproduces clearly: click
empty space below the bar, nothing happens until the bar moves.

Fix: subclass NSHostingView with a `visibleRegion: CGRect?` field; in
hitTest, return nil for points outside the silhouette so AppKit forwards
the click to whatever window is below. The SwiftUI side now reports the
silhouette's bounding rect on every mode/hover transition through a new
`onSilhouetteRectChanged` callback, so the clickable region tracks the
actual visible shape exactly.

Components
- IslandAppLib/Windows/ClickThroughHostingView.swift
  - Generic NSHostingView<RootView> subclass
  - `visibleRegion` is the only public surface; nil = default behavior
  - hitTest: convert incoming point to local coords, return nil if
    outside region, otherwise super.hitTest. Same coord space as
    SwiftUI since NSHostingView is flipped (top-left origin).
- IslandRootView
  - New required `containerWidth: CGFloat` parameter — needed to
    compute the silhouette's centered x position.
  - New optional `onSilhouetteRectChanged: ((CGRect) -> Void)?`
    callback. Fires from `.onChange(of: silhouetteRect, initial: true)`
    with rect = (x: (containerWidth - shapeWidth) / 2, y: 0,
    width: shapeWidth, height: shapeHeight).
- IslandWindow
  - Owns ClickThroughHostingView<IslandRootView> instead of plain
    NSHostingView. Wires the callback in both `init` and `reposition`
    so the rect stays in sync after screen changes too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(initial value race)

Previous click-through fix relied entirely on a SwiftUI
.onChange(silhouetteRect, initial: true) callback to push the rect
into ClickThroughHostingView.visibleRegion. That has a startup race:

  1. ClickThroughHostingView is created with a rootView whose closure
     captures `[weak self]`.
  2. SwiftUI mounts the view and fires .onChange(initial: true) right
     away — possibly BEFORE `self.hostingView = host` runs on the
     next line.
  3. Closure invokes `self?.hostingView.visibleRegion = rect`. At
     this point `self.hostingView` is still nil, so the assignment
     silently does nothing.
  4. After init, .onChange only fires on actual silhouette changes
     (mode flip, hover widen). If the user just sits at idle with
     no animation, the rect never updates.
  5. Result: visibleRegion stays nil → ClickThroughHostingView falls
     back to default hitTest → the whole 408pt window keeps eating
     clicks across the upper-middle of the screen.

Fix: compute the silhouette rect for the COLLAPSED, NOT-HOVERED state
explicitly in IslandWindow.idleSilhouetteRect(for:), and assign it to
hostingView.visibleRegion both at init (seed) AND at the end of every
reposition(). The SwiftUI callback still handles hover/mode transitions
on top of the seed.

Verified app launches and stays alive in smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… previous hitTest approach didn't actually intercept)

Take 2 on click-through. The NSHostingView-subclass + hitTest filter
landed in 68e54a0 / b9b17e5 didn't actually fix the bug — clicks
across the upper-middle of the screen still got swallowed by the
window's transparent area. Likely cause: SwiftUI / NSHostingView do
their own hit-testing through CALayer or sub-views in a way that
doesn't always route through the NSView.hitTest override, OR the
coordinate conversion via convert(_:from:superview) isn't always
returning what we'd expect. Either way, the override wasn't reliable.

New approach (the proven pattern, used by NotchKit and friends):
toggle the WINDOW's ignoresMouseEvents based on cursor position.

- Default: `ignoresMouseEvents = true`. The window is fully
  click-through. Clicks anywhere in the 408pt × 536pt frame fall
  straight through to the app below.
- A 25Hz timer polls `NSEvent.mouseLocation` and checks whether the
  cursor is inside the cached silhouette screen rect. If so, set
  `ignoresMouseEvents = false` so the bar / panel respond normally.
  As soon as the cursor leaves the silhouette, we flip back.

The silhouette rect comes from the existing `onSilhouetteRectChanged`
callback (kept from the previous attempt) — IslandRootView reports
its top-left local rect on every mode/hover change, IslandWindow
converts that into bottom-left screen coords once and caches it.
`reposition()` re-seeds the cache with the idle layout so the timer
has something to compare against immediately, before any SwiftUI
animation has fired.

Removed: IslandAppLib/Windows/ClickThroughHostingView.swift — the
NSHostingView subclass is no longer needed, plain NSHostingView is
what IslandWindow holds now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ask 5)

Posts macOS banner notifications when TaskStore tasks change status:

- running → completed → "Task Completed" banner
- any state → waiting → "Task Needs Input" banner

Tap a banner to open the task's URL in the default browser via
NSWorkspace.shared.open. URL is stashed in
UNNotificationContent.userInfo["taskURL"] and recovered in the
UNUserNotificationCenterDelegate callback.

Components
- IslandAppLib/Notifications/TaskNotifier.swift
  - @mainactor singleton conforming to UNUserNotificationCenterDelegate
  - withObservationTracking re-arming loop for @observable TaskStore.tasks
  - Baseline-then-diff: first observation pass populates
    previousTasksById without firing any banners (else every existing
    task at launch would spam a notification). From the second update
    onwards, only tasks that actually transitioned status fire.
  - Per-task identifier (`task-completed-<id>`, `task-waiting-<id>`)
    so a re-fire (status flapping on a flaky network, retry, etc)
    replaces the previous banner rather than stacking duplicates.
  - willPresent delegate forces banner+sound even when foreground
    (rare, since we're .accessory).

Bundle-support guard
- UNUserNotificationCenter.current() throws
  NSInternalInconsistencyException ("bundleProxyForCurrentProcess is
  nil") when launched directly from .build/debug — SPM doesn't produce
  a .app bundle with an Info.plist. We check
  Bundle.main.bundleURL.pathExtension == "app" before any UN call and
  silently skip authorization + posting in dev builds. Diff machinery
  still runs (so transition-detection bugs would still surface in
  logs). Real .app builds — including the eventual Homebrew Cask /
  Xcode-built artifact — get full notifications.

Wiring
- AppDelegate.applicationDidFinishLaunching kicks off
  TaskNotifier.shared.start() after the IslandWindow + sandbox + open-
  settings observer are wired up. Idempotent — start() is safe to call
  once and only once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ns to spec (Task 3 audit)

CLAUDE_CLIENT.md §6 task 3 requires '状态切换颜色过渡 250ms' across
the status palette and '胶囊外发光... 和圆点同相位' — the bar's outer
glow color should track the StatusDot's fill color in lockstep.

Two missed spots from the original implementation:

1. IslandRootView.backdrop — the bar/panel silhouette's glow shadow
   read `BarState.derive(from: store.tasks).color` per frame inside
   the TimelineView, with no `.animation(Motion.colorTransition,
   value:)` modifier. Effect: when state changed (e.g. running →
   completed), the StatusDot fill cross-faded over 250ms (its own
   .animation handles that), but the bar's outer glow snapped instantly
   to the new state color. Visually they were out of phase.
   NotchBarView's drawsBackdrop=true branch had the right pattern
   already; just porting it to IslandRootView's drawsBackdrop=false
   path.

2. NotchPanelView.connectionDot — the panel header's connection-status
   dot (green/yellow/red) had no color animation, so a connectionStatus
   transition (connected ↔ reconnecting ↔ disconnected) jump-cut. Adds
   the same Motion.colorTransition for consistency with the bar's
   StatusDot palette.

StatusDot itself was already spec-compliant: 5 states (idle/running/
waiting/completed/failed) matched scale + glow values, ripple layer
correct (3 rings, 0.5s phase offsets, 1.5s cycle), color transition
animation in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nnecting (Task 8)

CLAUDE_CLIENT.md §6 task 8: 'connectionStatus 为 disconnected/
reconnecting 时胶囊显示灰色'. Until now the bar's StatusDot fill +
silhouette glow showed the live BarState (blue running, yellow
waiting, etc.) regardless of whether the data was actually fresh —
even when the Manus connection was down or reconnecting and the
displayed tasks were stale.

Add a single computed property `effectiveBarState` that takes
`ConnectionStatus` into account:

  - .disconnected / .reconnecting → collapse to `.idle` (gray, no
    glow, no animation). Tells the user 'this readout might not be
    live right now'.
  - .connected / .degraded → return the raw BarState. `.degraded`
    means the webhook tunnel failed but PollingFallback is still
    refreshing every 60s, so the status colors are still meaningful.

Replaces every `BarState.derive(from: store.tasks)` call site in
IslandRootView (4 places: the bar passed to NotchBarView, the
backdrop's glow, needsTimelineGlow, and the synthetic-bar's
StatusDot) with `effectiveBarState`. The 250ms `Motion.colorTransition`
animations we already have on StatusDot fill + backdrop shadow handle
the dim-in / un-dim cross-fade smoothly when connection status flaps.

Verify via Sandbox: with running tasks visible, click 'Disconnected'
in the Connection row → bar fades to gray. Click 'Connected' →
bar fades back to running blue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eld notes

docs/manus-api-field-notes.md (from S's merge) records the actual
key format from live testing as `sk-…`, not the `mk_live_…` shown
in the public Manus docs example. Update the SecureField placeholder
so the user knows what to expect to paste.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the 242-byte ASCII placeholder from IslandCore Resources/ and
remove the SPM resource declaration. CloudflaredProcess now resolves
the binary at runtime in this order:

  1. /opt/homebrew/bin/cloudflared (Apple Silicon brew)
  2. /usr/local/bin/cloudflared (Intel brew)
  3. cloudflared on $PATH (env-based which lookup)

The Cask formula (added in a follow-up commit) declares
`depends_on cask: "cloudflared"` so brew install --cask users get the
binary at install time. Source builds and offline installs land in
step 3, or fail over to PollingFallback's 60s loop via the existing
TunnelManager catch.

Why this sequence:

- The previous Bundle.module path tried to exec a 242-byte text file
  during integration testing (sk- key end-to-end). POSIXErrorDomain=8
  "Exec format error" was the result; tunnel never came up.
- Bundling cloudflared (the alternative) would add ~30MB×2 archs to
  the .app and violate Homebrew Cask's "no bundled binaries with
  upstream brew formula" preference.
- PATH lookup uses /usr/bin/env rather than /usr/bin/which because
  env is more reliably present in sandboxed contexts and Apple has
  been quietly trimming the base /usr/bin set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NotchPanelView and TaskCard had #Preview blocks under #if PREVIEWS that
referenced TaskStore.previewTasks, which only exists inside a
#if DEBUG extension on TaskStore. Release builds (the Cask release
artifact path: swift build -c release inside scripts/build-app.sh)
have PREVIEWS defined (Package.swift sets it whenever Xcode is
installed, which on every dev mac is true) but not DEBUG, so these
files failed to compile under -c release.

Fix: gate the offending blocks behind `#if PREVIEWS && DEBUG`,
matching the pattern SettingsView.swift was already using. StatusDot's
#if PREVIEWS block is left alone because its previews don't reference
any DEBUG-gated symbols.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls together the shipping path for `brew install --cask island`:

  scripts/build-app.sh          - SPM binary → Universal Mach-O .app
  IslandApp/Resources/Info.plist - bundle metadata (LSUIElement, ids)
  VERSION                       - single-source-of-truth version string
  .github/workflows/release.yml - tag-triggered: build → sign → notarize
                                  → staple → zip → GitHub Release
  dist/homebrew-island/         - draft Cask formula + tap publishing
                                  guide (the actual tap is a separate
                                  repo; this directory holds the
                                  source-of-truth version)

The script vs xcodeproj decision is documented inline in build-app.sh:
SPM-native makes maintaining a parallel xcodeproj just for .app
wrapping wasteful; the bundle format is just a directory tree with a
plist, ~30 lines of shell handles it. Mature SPM-based menubar apps
(Maccy, Itsycal) ship the same way.

Local invocation produces an ad-hoc-signed bundle that launches with
right-click → Open. The release workflow re-signs with Developer ID
and runs notarytool + stapler so the final user-facing artifact has
zero Gatekeeper friction.

Required GitHub Secrets are documented at the top of release.yml:
APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD,
SIGNING_CERTIFICATE_P12_BASE64, SIGNING_CERTIFICATE_P12_PASSWORD,
KEYCHAIN_PASSWORD.

Cask depends_on cask: "cloudflared" so realtime webhook tunnel is the
default install experience; CloudflaredProcess (previous commit)
gracefully degrades to 60s polling if the binary's missing.

build/ added to .gitignore — release artifacts live on GitHub
Releases, never in git.

Smoke-tested locally: `./scripts/build-app.sh` produces a valid
universal Island.app, codesign --verify --strict passes ad-hoc
("valid on disk", "satisfies its Designated Requirement").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- release.yml release notes: lead with manual install (the primary
  path via devisland.app) instead of brew. Brew section reframed as
  "optional, when the tap is published" since the homebrew-island tap
  repo isn't created yet.
- Cask formula: url + homepage point at sheepxux/Dev-Island and
  devisland.app respectively.
- README: <your-handle> placeholders → sheepxux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source: 2048×2048 PNG (devislandlogo) — dark capsule + pixel `D_`
display matching the app's notch-bar visual language.

scripts/make-icon.sh takes any square PNG and produces all 10 sizes
Apple's iconutil expects (16/32/128/256/512 × 1x+2x), then compiles
to .icns and drops it at IslandApp/Resources/AppIcon.icns where
build-app.sh's existing resource-copy step picks it up automatically.

Why commit the .icns rather than regenerate in CI:

- Source PNG (2.5MB) bigger than the compiled .icns (955KB)
- CI doesn't have to keep ImageMagick / sips invariants in sync
- Updating the icon is a one-time event per release, not per-build

If we ever need to regenerate (new design pass), drop the new PNG
anywhere on disk, run scripts/make-icon.sh <path>, commit the
resulting .icns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI was failing at the Setup signing keychain step with:

    security: SecKeychainItemImport: Unknown format in import.

`security import -t cert -f pkcs12` is contradictory: `-t cert` says
"treat as cert-only blob" but a PKCS#12 file is an aggregate of cert
plus private key. The right type is `-t agg`.

Two follow-up improvements while we're in here:

1. Print `file ${CERT_PATH}` and `ls -la` before the import call.
   The previous failure mode looked like a credential issue but was
   actually a flag mismatch — surfacing the decoded file's actual
   format makes future regressions diagnose-able from the log alone.

2. Add `-T /usr/bin/codesign` and `-T /usr/bin/security` to the import
   so subsequent codesign / notarize calls don't trip the
   "this private key wants to be used by codesign, please confirm"
   interactive prompt that CI runners can't answer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues from the v0.1.0 retag attempt:

1. swift-async-algorithms 1.1.3 (transitive via Hummingbird 2.16) uses
   typed throws (`throws(Failure)`) and the `#isolation` macro, both
   Swift 6 features. The previous Xcode_15.4 selection on macos-14 ships
   Swift 5.10 and chokes at AsyncFlatMapLatestSequence.swift:85 with
   "consecutive declarations on a line must be separated by ';'".

   Fix: switch runner to macos-15, which ships Xcode 16 (Swift 6) by
   default. Drop the explicit Xcode_X.Y path because GitHub rotates
   point releases and the path can disappear without notice; the
   default toolchain is fine as long as it's >=6.0.

2. SPM emitted a warning during build:

       found 2 file(s) which are unhandled
           IslandApp/Resources/Info.plist
           IslandApp/Resources/AppIcon.icns

   These files live under IslandApp/Resources/ for build-app.sh to
   copy into the bundle's Contents/, but they're not swift-side
   resources (the bare SPM executable is wrapped, not shipped as an
   SPM bundle). Add them to the IslandApp target's `exclude:` list so
   SPM ignores them entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.0 second attempt notarized .app fine on our side but timed out
waiting for Apple after 20 minutes — submission b3b8e1d3 was still
"In Progress" when notarytool gave up. New Developer IDs without
reputation routinely hit longer queues; 20m was too tight.

  notarytool --timeout 20m → 45m
  job timeout-minutes 30   → 60

Median Apple turnaround is 5-10 min; the new 45m gives generous
slack for tail latencies. Job-level 60m bounds total runtime if
Apple's service has a real outage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframes the README around the user-facing product first (what it is,
what it looks like, how to install), pushing technical detail to the
back. Specific changes:

- Lead with the app icon and a one-line tagline
- "Why this exists" pitch — keep tab-context for background agent work
- 5-state color table with priority order (waiting > failed > running
  > completed > idle)
- Three install paths, ordered by accessibility:
  1. devisland.app download (the main path)
  2. Homebrew Cask (coming soon, formula draft already in
     dist/homebrew-island/)
  3. From source (swift run / build-app.sh)
- First-run setup: gear → paste sk-... → Connect, in three steps
- "How sync works" explains the webhook → polling fallback graceful
  degradation, so users on locked-down networks know what to expect
- Architecture: same target table as before, plus an ASCII data-flow
  diagram showing UI ↔ TaskStore ↔ services
- Sources table reframed as ✅/🚧/📋 status
- Status section honest about what ships vs. what's in flight

Visual assets:
- docs/media/logo.png — 2048×2048 master from designer
- docs/media/logo-256.png — sized variant for the README header
- (existing) docs/media/dev-island-hero.svg — banner kept as-is
- (existing) docs/media/status-priority-demo.gif — state demo kept

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v0.1.0 third attempt got the entire pipeline through — including
the previously slow notarize step (41s, Apple now warm for our
account) — only to die at Create GitHub Release with:

    403 Resource not accessible by integration
    Skip retry — your GitHub token/PAT does not have the required
    permission to create a release

Since GitHub tightened the default GITHUB_TOKEN scopes, workflows
have to opt into write permissions explicitly. Adding `contents:
write` at workflow level lets softprops/action-gh-release POST to
/repos/.../releases.

`contents: write` is the minimum scope; we deliberately avoid the
full `permissions: write-all` so any future action regressions can't
silently exfiltrate via this token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.0 ships only as Island-0.1.0.zip, which means the landing page's
download URL has to be bumped on every release. Add a second asset
named Island.zip (literal copy, same bytes, same notarization) so:

    https://github.com/sheepxux/Dev-Island/releases/latest/download/Island.zip

becomes a permanent URL — devisland.app links to that and never needs
touching again.

Both filenames remain on each release: Island.zip is the convenient
default, Island-<VERSION>.zip is the version-pinned archive useful
for issue triage and reproducibility.

The current v0.1.0 release was patched manually via gh CLI to add
Island.zip; this commit makes future releases produce both
automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sland, brew dev-island, fuller icon

Three brand-consistency changes for v0.1.1:

1. Logo fills the icon canvas
   The v0.1.0 source PNG had ~30% transparent padding around the dark
   squircle design, so the rendered macOS icon showed a small capsule
   floating inside macOS's default rounded-square container — looked
   under-sized in Finder and the Dock. Trim the source to its
   non-transparent bounds (1562×1562 from a 2048×2048 canvas) and
   regenerate AppIcon.icns. Now the design is the icon shape;
   macOS's rounded-square mask aligns with the user's squircle.
   docs/media/logo-trimmed.png is the trimmed master, fed to
   scripts/make-icon.sh.

2. Display name "Island" → "Dev Island"
   Product name on devisland.app is "Dev Island", but the macOS bundle
   was just "Island". Updated CFBundleName + CFBundleDisplayName so
   Finder, About menu, Force Quit dialog, etc. all show "Dev Island".
   The Swift target / executable name stays "IslandApp" — it's a
   build-system identifier, not user-facing.

3. Bundle identifier com.island.app → app.devisland.Island
   Reverse-DNS of the actual product domain (devisland.app's TLD is
   .app, so reverse is `app.devisland.X`). Updated everywhere it was
   hard-coded:

     - Info.plist CFBundleIdentifier
     - scripts/build-app.sh BUNDLE_ID + codesign -i flag
     - .github/workflows/release.yml codesign -i flag
     - IslandCore.KeychainStore.service (where the Manus API key lives)
     - IslandCore.TunnelManager UserDefaults suite (webhook id)
     - IslandLogger subsystem (api/webhook/tunnel/store/sync/storage)
     - TaskNotifier logger subsystem
     - dist/homebrew-island Cask zap paths

   ⚠️ Existing v0.1.0 installs lose their Keychain entry + UserDefaults
   suite on upgrade — only affects the developer's own test install
   right now, future users land on v0.1.1+ from the start.

4. Cask name `island` → `dev-island`
   The brew tap will be `sheepxux/dev-island` (repo: homebrew-dev-island)
   and the install command `brew install --cask dev-island`. Renamed
   the formula file via `git mv` so history follows. README + workflow
   release notes updated to match.

VERSION bumped 0.1.0 → 0.1.1. v0.1.0 stays public as the first
ship; v0.1.1 lands the rename + icon fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Designer delivered a redesigned logo: light-on-dark terminal squircle
with a chunky `>_` glyph instead of the original pixel `D_`. Visually
heavier, fills the canvas tighter, reads better at small sizes (the
Dock thumbnail and menubar status are both <40pt).

Source: 1000×1000 PNG. Below the macOS 1024×1024 recommendation, so
the 512@2x slice (1024×1024) gets a 2.4% upscale via sips. Visible
softness on Retina is minimal but real; if it ever bothers anyone,
ask the designer to bump the source to 1024+.

Pipeline same as before: PIL trims any transparent border, scripts/
make-icon.sh sips into the 10 required sizes, iconutil compiles
AppIcon.icns. Smoke-tested locally — build-app.sh produces a 39MB
universal Island.app with the new icon embedded, codesign --verify
passes ad-hoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…the right name everywhere

The previous v0.1.1 attempt set CFBundleDisplayName to "Dev Island"
but kept the bundle filename as `Island.app`. Finder uses display
name only in /Applications, the Dock, and About menus — anywhere
else (Downloads, arbitrary folders, Spotlight) it falls back to the
filename minus `.app`, which was still "Island". User saw an icon
captioned "Island" under their dock thumbnail and reasonably asked
why the rename didn't take.

Fix: rename the bundle filename itself to "Dev Island.app". Now
Finder shows "Dev Island" regardless of where the .app lives.

Touched everywhere the bundle path was hard-coded:

  - scripts/build-app.sh APP="${BUILD_DIR}/Dev Island.app"
  - .github/workflows/release.yml — codesign / spctl / ditto /
    stapler all quote the path because of the embedded space
  - dist/homebrew-island/Casks/dev-island.rb app "Dev Island.app"
  - README.md install instructions

The release zip's CONTENTS now unpack to "Dev Island.app". Cask
formula's `app "Dev Island.app"` directive matches. Everywhere a
script referenced the path, it's now quoted ("build/Dev Island.app")
so the space doesn't get word-split.

Smoke-tested locally: build-app.sh produces a 39MB universal
"Dev Island.app", codesign --verify --strict passes ad-hoc, plist
fields all read correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…istency

The asset filename was a leftover from before today's rename:
Island.zip / Island-<VERSION>.zip while the product, repo, and Cask
all say "Dev Island" / "Dev-Island" / "dev-island". User caught the
inconsistency on the v0.1.1 release page asking why the download
isn't named Dev-Island.

Hyphen, not space:
- URLs with %20 encoding look untrustworthy when hovered
- Matches the GitHub repo name (Dev-Island) so the entire URL reads
  consistently: github.com/.../Dev-Island/.../Dev-Island.zip
- Inside the zip the bundle is still "Dev Island.app" with the space
  (matches CFBundleDisplayName); only the wrapper filename changes

Updated everywhere the asset name was hard-coded:
  - .github/workflows/release.yml — package step + release notes +
    intermediate notarization zip
  - dist/homebrew-island/Casks/dev-island.rb — url stanza
  - dist/homebrew-island/README.md — publishing flow doc

v0.1.1's previously-published Island.zip is being deleted along with
the release; CI will republish v0.1.1 with Dev-Island.zip in the
same retag cycle. v0.1.0 stays untouched (still ships Island.zip
under its own tag URL — only the latest URL switches).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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