Notch island port + threads panel — Implementation Plan
Goal: Replace the rejected floating-pill island with a proper notch-hugging island ported from
vibe-notch(Apache-2.0), adapted to North OS: closed-notch activity for voice turns + proactive nudges, hover/click to open a panel showing the lawyer's threads (past + active), bounce + sound + click-through + multi-screen. NewGET /threadsgateway route feeds the panel.
Source: ~/workspace/transformative_studio/vibe-notch (ClaudeIsland, Apache-2.0). We port the notch core verbatim where it's generic geometry/windowing, and ADAPT the view-model + views to North's signals (no Claude-CLI concepts).
Split: Backend (GET /threads) built + tested here. Swift ported + adapted here, compiled by Christophe in Xcode (xcodebuild barred). New .swift files auto-compile (project uses PBXFileSystemSynchronizedRootGroup — no pbxproj edit).
Phase 0 — Remove the rejected pill
Delete IslandState.swift, IslandPanel.swift, IslandView.swift (the floating-pill files). Keep ProactiveContextMonitor.swift (its ProactiveGlanceClientResult + governor are reused). Remove the island wiring from CompanionManager that referenced islandManager: IslandManager / IslandState — it'll be rewired to the notch view model in Phase 3.
Commit: refactor(polaris): remove rejected floating-pill island (keep proactive monitor).
Phase 1 — Backend: GET /threads
Task 1.1 — route test (fails)
apps/clicky-gateway/src/threads.test.ts — hoisted-mock ./session-resolver + @workspace/chat-runtime (listThreads); assert 401 on no session, 200 + JSON array for an authed request. Mirror proactive-glance.test.ts exactly.
Task 1.2 — route
In router.ts: clickyRouter.use("/threads", clickyAuth) + a GET /threads handler that calls listThreads(organizationId, userId) and maps each to { id, title, agentId, matterId, updatedAt: iso, deepLink: webBase + "/tasks/chats/" + id }. Reuse PROACTIVE_WEB_BASE.
Import listThreads from @workspace/chat-runtime (already exported).
Run via bun run test src/threads.test.ts (dotenv-wrapped) → green. Then full gateway suite. Then root typecheck + lint.
Commit: feat(clicky-gateway): GET /threads route for the notch panel.
Phase 2 — Port the notch core (Swift, generic)
New group apps/polaris/leanring-buddy/Notch/. Each ported file gets a header crediting vibe-notch (Apache-2.0). These are ported with minimal change (generic geometry/windowing):
Notch/NotchShape.swift — verbatim (inward-curving notch path).
Notch/NotchGeometry.swift — verbatim (notch rect, opened rect, hit tests).
Notch/Ext+NSScreen.swift — verbatim (notchSize, builtin, hasPhysicalNotch).
Notch/EventMonitor.swift + Notch/EventMonitors.swift — verbatim (global mouse monitors). NOTE: needs the existing accessibility permission (already granted) for global event taps.
Notch/NotchWindow.swift — the NotchPanel (above-menu-bar, click-through, event re-post). Verbatim.
Notch/NotchViewController.swift — PassThroughHostingView host. Adapt the generic NotchView type ref to ours.
Notch/NotchActivityCoordinator.swift — the closed-state expanding-activity coordinator. Adapt NotchActivityType: replace .claude with North's .working + add .nudge.
Notch/Components/ProcessingSpinner.swift — verbatim (recolor to North gold via DS).
Notch/Components/NotchStatusIcons.swift — a small North adaptation of StatusIcons: a working dot, a nudge dot, a done checkmark (use SF Symbols / simple shapes, DS colors — drop the pixel-art crab).
Commit: feat(polaris): port vibe-notch notch core (shape, window, geometry, events).
Phase 3 — Adapt the view model + view to North
Task 3.1 — Notch/NotchViewModel.swift (adapted)
Port NotchStatus (closed/popping/opened) + hover/click handling verbatim. Replace NotchContentType with North's: .activity (default closed state), .threads (the opened panel), .menu (settings). Drop all Claude-CLI session concepts. Add published North state: workingLabel: String? (the ack sentence), pendingNudge: (text: String, deepLink: URL?)?. openedSize: threads ≈ 480×360, menu ≈ 480×320.
Task 3.2 — Notch/NotchView.swift (adapted)
Port the shape/expand/header structure. Closed-activity header driven by North signals:
// left icon + right spinner/checkmark widen the closed notch
// working → North mark left, ProcessingSpinner right
// nudge → subtle dot left, nudge glyph right (silent), bounce
// answer → checkmark right, bounce, then settle
// idle → bare notch (invisible after delay on notched Macs)
Bounce animation on new event (port isBouncing spring). Notification sound on answer-ready / nudge when Polaris not frontmost (port the NSSound path, behind an AppSettings-style flag; reuse the app's existing sound asset if present, else a system sound).
Opened content: case .threads → NorthThreadsView, case .menu → a minimal settings view (sound toggle + quit, for now). Header has a menu toggle like the original.
Task 3.3 — Notch/Views/NorthThreadsView.swift (new)
A scrolling list of the lawyer's threads (newest first). Each row: title + relative time + agent glyph; tap → NSWorkspace.shared.open(deepLink) and close the notch. Active thread (the in-flight activeDesktopThreadId) pinned/highlighted at top. Empty state: "No conversations yet".
A NotchThreadsStore (ObservableObject): fetches GET /threads via a new NorthDispatchAPI.fetchThreads(), refreshes on notch-open + every ~30s while open. Reuses the bearer/401 posture.
Commit: feat(polaris): adapt notch view-model + view to North + threads panel.
Phase 4 — Wire into the app
Task 4.1 — NorthDispatchAPI.fetchThreads()
GET /threads client method → [NotchThread] (id, title, agentId, deepLink: URL, updatedAt: Date). Mirror proactiveGlance's bearer + 401 handling.
Task 4.2 — window lifecycle
Port a slimmed NotchWindowManager + ScreenObserver (or fold into CompanionManager): build the NotchWindowController on the built-in screen, recreate on screen-change (debounced). Boot animation (expand→collapse) on launch. Own the NotchViewModel so CompanionManager can push signals into it.
Task 4.3 — CompanionManager rewire (replaces the deleted pill wiring)
Hold the NotchViewModel (via the window manager). In the voice-state sink: .processing/.responding → coordinator.showActivity(.working) + set vm.workingLabel; .idle → coordinator.hideActivity(). In onAck: vm.workingLabel = ack. At answer-ready: trigger the done-checkmark + bounce, and ensure the thread is reachable (the panel will list it).
Rewire ProactiveContextMonitor's surface closure: instead of the old islandManager.update(.message), set vm.pendingNudge = (text, deepLink) + coordinator.showActivity(.nudge) + bounce (silent — no TTS). And isIslandFree → vm.status == .closed && coordinator.expandingActivity.show == false.
Update apps/polaris/AGENTS.md Key Files: remove the 3 pill rows, add the Notch/* files.
Commit: feat(polaris): wire notch island into app (window, screens, voice + proactive signals).
Verification
- Backend (here):
GET /threadstest green; full gateway suite green; root typecheck + lint clean. - Client (Christophe, Cmd+R): (1) the island IS the notch — sits on the camera housing, not a floating pill. (2) A voice turn: the closed notch widens with a spinner + the ack sentence; on answer, a checkmark + bounce. (3) Hover the notch → it expands into the threads panel; tap a thread → opens
/tasks/chats/<id>. (4) Switch to Mail + wait the settle → a silent nudge widens the notch + bounce; tap opens the deep-link. (5) External monitor / non-notched Mac → island still present (fallback shape, always visible). - Possible server need: the
GET /threadsroute ships inclicky-gateway— if Christophe tests against the cosmic dev server, the gateway there needs a redeploy (flag to Christophe at the end; do NOT auto-deploy). - Spec: flip to
verified (prototype)after the above, noting as-built deltas.