[C] Task 1: Notch Bar — adaptive shape, synthetic notch, hover affordance#2
[C] Task 1: Notch Bar — adaptive shape, synthetic notch, hover affordance#2sheepxux wants to merge 60 commits into
Conversation
- 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>
There was a problem hiding this comment.
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.
…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>
There was a problem hiding this comment.
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.
| 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 } | ||
| } | ||
| } |
There was a problem hiding this comment.
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>
| 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 } | |
| } | |
| } |
| 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) | ||
| } |
There was a problem hiding this comment.
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>
| 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) | |
| } |
There was a problem hiding this comment.
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.
|
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 |
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>
…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>
Summary
Task 1 complete: collapsed Notch Bar with full design polish.
Visual
frame.maxY - visibleFrame.maxYfor accurate measurement).Hover affordance
.pointingHand.Adaptivity
didChangeScreenParametersNotification— display swap, "Larger Text", scale changes are all auto-handled.Architecture
IslandApp(exec, @main + AppDelegate) +IslandAppLib(library, all view code) so SwiftUI Previews work under Xcode 26'sENABLE_DEBUG_DYLIBrequirement.TaskStorefromIslandCoreper the contract.Test plan
NotchBarView.swift→ Canvas ResumeSummary by cubic
Builds the collapsed Notch Bar with adaptive/synthetic notch, hover affordance, and a single‑window bar→panel morph backed by live
IslandCoredata. 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
ManusAPIClientwithHummingbirdwebhook + tunnel and 60s polling fallback; macOS banners for running→completed and any→waiting; taps open task URLs; enabled only in bundled.appbuilds.SMAppService, Quit; gear opens it; API key placeholder updated tosk‑….IslandWindowmorphs a sharedNotchPanelShape; transparent areas click‑through via cursor‑trackedignoresMouseEvents; 250ms synced color transitions; bar dims to idle gray when.disconnected/.reconnecting(.degradedkeeps live colors).Release & Build
app.devisland.Island; new AppIcon; Homebrew Cask renamed todev-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.Dev-Island.zipandDev-Island-<VERSION>.zip; printssha256for the Cask..app:scripts/build-app.shwraps the executable; App icon bundled; Info.plist added; build output ignored inbuild/.dist/homebrew-island; Cask declaresdepends_on cask: "cloudflared".cloudflaredresolution at runtime:/opt/homebrew/bin→/usr/local/bin→$PATH; falls back to 60s polling if missing..p12import flags, longer notarize timeouts,contents: writepermissions; previews guarded with#if PREVIEWS && DEBUG.Written for commit a635299. Summary will update on new commits.