← plans · north_os · DESIGN.md11 juin 2026

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. New GET /threads gateway 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.swiftPassThroughHostingView 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 isIslandFreevm.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