Skip to content

M6: onboarding, app bundle scripts, shortcuts#77

Open
kalil0321 wants to merge 10 commits into
claude/proxy-monitor-m5-agent-sidecarfrom
claude/proxy-monitor-m6-polish-distribution
Open

M6: onboarding, app bundle scripts, shortcuts#77
kalil0321 wants to merge 10 commits into
claude/proxy-monitor-m5-agent-sidecarfrom
claude/proxy-monitor-m6-polish-distribution

Conversation

@kalil0321
Copy link
Copy Markdown
Owner

@kalil0321 kalil0321 commented May 19, 2026

Final stack PR — distribution polish.

What's in

  • OnboardingView — 3-step welcome sheet shown once on first launch (gated by @AppStorage("hasCompletedOnboarding")). Each step has a button that triggers the corresponding action, with live "Installed / Active / Capturing" state.
  • Keyboard shortcuts — ⌘R toggle capture, ⌘K clear flows, ⇧⌘E export HAR.
  • Resources/Info.plist — bundle metadata, LSMinimumSystemVersion=14.0, AppleEvents usage description for the networksetup prompt.
  • Resources/ReverseAPI.entitlements — hardened-runtime compatible, no sandbox (the proxy + Keychain + AppleScript flow needs full access), network.client + network.server + automation.apple-events.
  • scripts/build-app.sh — universal swift build (arm64 + x86_64), assembles build/ReverseAPI.app/Contents/{MacOS,Info.plist,Resources}.
  • scripts/sign-and-notarize.sh — codesign with SIGNING_IDENTITY, deep verify, optional NOTARY_PROFILE-driven notarytool submit --wait + staple.
  • scripts/make-dmg.shhdiutil DMG with /Applications symlink, optional resigning of the DMG.

Release flow

cd macos
./scripts/build-app.sh
SIGNING_IDENTITY="Developer ID Application: …" \
NOTARY_PROFILE="rae-notary" \
  ./scripts/sign-and-notarize.sh
./scripts/make-dmg.sh

Drops build/ReverseAPI.dmg. Upload to a GitHub release and ship.

Out of scope

  • App icon (placeholder; add Resources/AppIcon.icns)
  • Auto-update (Sparkle) — can layer in if needed
  • Python sidecar bundling — backend still lives at ~/Library/Application Support/ReverseAPI/...-relative or PATH's python3 for now; bundling python-build-standalone is a follow-up

Generated by Claude Code


Summary by cubic

Adds a first-launch onboarding flow, a universal macOS app bundle with an embedded Python runtime, and scripts to sign/notarize and package a DMG. Polishes branding and reliability with light/dark themes, a splash screen, Fraunces wordmark, and crash-safe proxy recovery.

  • New Features

    • Onboarding: 3-step sheet (trust CA → enable system proxy → start capture) with live status; shown once via @AppStorage("hasCompletedOnboarding").
    • Reliability: persist pre-app proxy snapshot to disk before enabling; only load/restore if the system proxy points at our port; SIGINT/SIGTERM/SIGHUP handlers restore the proxy before exit.
    • UI/Brand: dynamic light/dark theme; Fraunces italic bundled and registered at app init; section headers use it; composer input is brighter with a 1pt border and a brand‑pink send button; capture dot is brand‑pink with a gentle pulse; search button gets a pink hover accent; status chips use a softer elevated background.
    • Launch splash: short “rae” splash with a subtle asterisk animation that fades into the main window.
  • Distribution

    • Bundle: Resources/Info.plist (min macOS 14.0, AppleEvents usage, automatic/sudden termination disabled) and Resources/rae.entitlements (Hardened Runtime; no sandbox; network.client, network.server, automation.apple-events).
    • Build: scripts/build-app.sh (universal arm64/x86_64; outputs macos/build/rae.app; copies static Info.plist; copies SwiftPM resource bundle ReverseAPI_ReverseAPI.bundle for fonts; embeds Python at Contents/Resources/agent-runtime; installs backend; trims caches; unsets proxy env vars before uv).
    • Signing/Packaging: scripts/sign-and-notarize.sh (codesigns with entitlements; explicitly signs nested runtime components; optional notarytool submit + staple) and scripts/make-dmg.sh (DMG with /Applications symlink; EXIT-trap cleanup; optional DMG signing).

Written for commit 0d92412. Summary will update on new commits. Review in cubic

Greptile Summary

This PR delivers the final distribution layer for the app: a first-launch onboarding sheet, keyboard shortcuts, a production-ready Info.plist and trimmed entitlements file, and three shell scripts to build a universal app bundle, codesign/notarize it, and package it as a DMG. It also adds an important reliability improvement — the pre-proxy snapshot is now persisted to disk so a crash or abrupt kill can restore the user's original network settings on next launch.

  • Onboarding flow: OnboardingView is a 3-step sheet gated by @AppStorage(\"hasCompletedOnboarding\"); both footer buttons explicitly set the flag before calling dismiss(). However, the set: closure on the isPresented binding in ContentView still fires when the sheet is dismissed via Escape, silently marking onboarding complete without any setup step.
  • Crash-recovery: applySystemProxy now writes the snapshot to disk before enabling the proxy and deletes it on every clean restore/disable path; SIGINT/SIGTERM/SIGHUP dispatch sources call restoreProxyBeforeExit before re-raising.
  • Distribution scripts: make-dmg.sh correctly places the EXIT trap immediately after mktemp; sign-and-notarize.sh drops deprecated --deep and signs nested runtime components explicitly; entitlements have been stripped to the minimal required set.

Confidence Score: 4/5

Safe to merge once the onboarding sheet binding is fixed; all other changes are additive and well-guarded.

The sheet binding set: closure still silently marks onboarding complete when the user presses Escape, leaving CA trust, system proxy, and capture steps uncompleted with no way for the sheet to reappear on next launch. Everything else — crash-recovery snapshot, signal handlers, trimmed entitlements, and distribution scripts — looks correct.

macos/Sources/ReverseAPI/UI/ContentView.swift — the isPresented binding set: clause needs to be a no-op and .interactiveDismissDisabled() added to the sheet content.

Important Files Changed

Filename Overview
macos/Sources/ReverseAPI/UI/ContentView.swift Adds onboarding sheet wiring; the binding's set: closure still marks completion on Escape dismiss, violating the explicit design intent
macos/Sources/ReverseAPI/UI/OnboardingView.swift New 3-step onboarding sheet; explicit button handlers correctly set the completion flag and call dismiss()
macos/Sources/ReverseAPI/App/AppState.swift Adds disk-backed proxy snapshot for crash-recovery; snapshot is written before enabling proxy and deleted on every clean exit/restore path
macos/Sources/ReverseAPI/App/ReverseAPIApp.swift Adds SIGINT/SIGTERM/SIGHUP dispatch-source handlers to restore proxy before process exit; re-raises the signal for correct exit status
macos/Resources/rae.entitlements Entitlements file trimmed to the minimal required set: sandbox=false, network.client/server, automation.apple-events
macos/Resources/Info.plist NSSupportsAutomaticTermination and NSSupportsSuddenTermination both set to false; LSMinimumSystemVersion=14.0 and NSAppleEventsUsageDescription added
macos/scripts/build-app.sh Updated for universal arm64+x86_64 build; copies static Info.plist and optional AppIcon from Resources/
macos/scripts/sign-and-notarize.sh New script; drops deprecated --deep for signing, signs nested runtime components explicitly, EXIT trap cleans up ZIP artifact
macos/scripts/make-dmg.sh New script; EXIT trap for STAGING directory placed immediately after mktemp, correctly covers all failure paths
macos/Sources/ReverseAPIProxy/System/SystemProxyController.swift Adds Codable conformance and explicit memberwise initializer to ProxyServiceSnapshot to support JSON disk persistence
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
macos/Sources/ReverseAPI/UI/ContentView.swift:110-115
**Sheet `set:` closure still marks onboarding complete on Escape**

The binding's `set: { if !$0 { hasCompletedOnboarding = true } }` runs any time SwiftUI transitions `isPresented` to `false`, including when the user presses Escape to dismiss the sheet. On macOS this is a natural keyboard shortcut for closing dialogs, so a user who presses Escape without completing any setup step has their onboarding silently flagged as done — contradicting the code comment's stated guarantee ("closing the window before acknowledging doesn't silently mark it complete").

Fix: make `set:` a no-op and add `.interactiveDismissDisabled()` to `OnboardingView`. The explicit button handlers in `OnboardingView` already set `hasCompletedOnboarding = true` before calling `dismiss()`, so the binding's `get:` will return `false` on the next render and the sheet will stay closed. Any non-button dismiss path (Escape, window close) will leave `hasCompletedOnboarding = false` and the sheet will reappear on next launch.

```suggestion
        .sheet(isPresented: Binding(
            get: { !hasCompletedOnboarding },
            set: { _ in } // completion is set only by explicit button actions inside OnboardingView
        )) {
            OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
                .interactiveDismissDisabled()
        }
```

Reviews (4): Last reviewed commit: "fix(macos): gate persisted snapshot load..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

2 issues found across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread macos/Resources/ReverseAPI.entitlements Outdated
Comment thread macos/scripts/make-dmg.sh
Comment thread macos/scripts/make-dmg.sh
Comment thread macos/Resources/ReverseAPI.entitlements Outdated
Comment thread macos/Sources/ReverseAPI/UI/ContentView.swift Outdated
@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 19, 2026

⚠️ Error — The test run failed unexpectedly.

Grok 4 Fast is deprecated. xAI recommends switching to Grok 4.3 (https://openrouter.ai/x-ai/grok-4.3)

This is likely a transient issue. You can re-trigger a run from the dashboard.

kalil0321 pushed a commit that referenced this pull request May 19, 2026
Fixes for PR #77 review comments (greptile, cubic):

- OnboardingView now takes a Binding<Bool> for hasCompletedOnboarding
  and flips it from the "Get started" button callback (in addition
  to dismiss). ContentView no longer relies on .onDisappear, which
  could fire during window destruction even if the user never
  explicitly acknowledged onboarding.
- ReverseAPI.entitlements: drop allow-jit, allow-unsigned-executable-
  memory, and disable-library-validation. Hardened Runtime stays
  strict — the app does not need JIT or to load unsigned dylibs.
  network.client / network.server / automation.apple-events (for
  the AppleScript admin prompt) and sandbox=false are kept.
- scripts/make-dmg.sh: install an EXIT trap that cleans up the
  staging directory created by mktemp -d, so failed hdiutil runs
  do not leak temp files.
Copy link
Copy Markdown
Owner Author

@greptile review


Generated by Claude Code

Comment thread macos/Resources/Info.plist Outdated
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m5-agent-sidecar branch from 7d880c6 to 4acdba7 Compare May 19, 2026 12:59
kalil0321 pushed a commit that referenced this pull request May 19, 2026
Fixes for PR #77 review comments (greptile, cubic):

- OnboardingView now takes a Binding<Bool> for hasCompletedOnboarding
  and flips it from the "Get started" button callback (in addition
  to dismiss). ContentView no longer relies on .onDisappear, which
  could fire during window destruction even if the user never
  explicitly acknowledged onboarding.
- ReverseAPI.entitlements: drop allow-jit, allow-unsigned-executable-
  memory, and disable-library-validation. Hardened Runtime stays
  strict — the app does not need JIT or to load unsigned dylibs.
  network.client / network.server / automation.apple-events (for
  the AppleScript admin prompt) and sandbox=false are kept.
- scripts/make-dmg.sh: install an EXIT trap that cleans up the
  staging directory created by mktemp -d, so failed hdiutil runs
  do not leak temp files.
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m6-polish-distribution branch from 519d22e to b8a7142 Compare May 19, 2026 13:00
Copy link
Copy Markdown
Owner Author

@greptile review


Generated by Claude Code

@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m5-agent-sidecar branch from 0f750b6 to 6a1d716 Compare May 19, 2026 13:21
kalil0321 pushed a commit that referenced this pull request May 19, 2026
Fixes for PR #77 review comments (greptile, cubic):

- OnboardingView now takes a Binding<Bool> for hasCompletedOnboarding
  and flips it from the "Get started" button callback (in addition
  to dismiss). ContentView no longer relies on .onDisappear, which
  could fire during window destruction even if the user never
  explicitly acknowledged onboarding.
- ReverseAPI.entitlements: drop allow-jit, allow-unsigned-executable-
  memory, and disable-library-validation. Hardened Runtime stays
  strict — the app does not need JIT or to load unsigned dylibs.
  network.client / network.server / automation.apple-events (for
  the AppleScript admin prompt) and sandbox=false are kept.
- scripts/make-dmg.sh: install an EXIT trap that cleans up the
  staging directory created by mktemp -d, so failed hdiutil runs
  do not leak temp files.
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m6-polish-distribution branch from db29419 to d7e7ff0 Compare May 19, 2026 13:21
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m5-agent-sidecar branch from 6a1d716 to 1d8eb1f Compare May 19, 2026 23:54
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m6-polish-distribution branch from a07e9be to 17a107c Compare May 20, 2026 13:40
Distribution polish on top of M5. End-user experience now covers:
first-launch setup, a clean .app the OS recognizes as a regular GUI
app, and a signed/notarized DMG ready to upload to a GitHub release.

Onboarding (new):
- UI/OnboardingView.swift — sheet shown on first launch (gated by
  @AppStorage("hasCompletedOnboarding")) that walks the user through
  three steps with live "Trusted / Routed / Capturing" indicators:
  1. Trust the local root certificate (Keychain)
  2. Route the system through the proxy (networksetup via AppleScript)
  3. Start capturing traffic
  Each step button triggers the matching AppState action and the
  step's check + pill flip green as the underlying state updates.
- ContentView wires the sheet via .sheet(isPresented:). The flag is
  flipped from inside the "Get started" / "Skip for now" handlers,
  not from .onDismiss, so closing the window before acknowledging
  doesn't silently mark onboarding done.
- Themed against Theme.surface / .elevated / .success / .accent so
  the sheet feels like the rest of the app rather than the default
  system sheet chrome.

App bundle metadata (new):
- Resources/Info.plist — CFBundleIdentifier=app.rae.reverseapi,
  LSMinimumSystemVersion=14.0, NSAppleEventsUsageDescription
  explaining the networksetup elevation prompt. Crucially
  NSSupportsAutomaticTermination=false +
  NSSupportsSuddenTermination=false so macOS doesn't reap the
  process while a capture is running in the background.
- Resources/rae.entitlements — Hardened Runtime strict (no sandbox
  because the proxy + Keychain + AppleScript flow needs full
  access), with just network.client + network.server +
  automation.apple-events. No allow-jit / allow-unsigned-executable-
  memory / disable-library-validation — the app doesn't need them.

Scripts:
- scripts/build-app.sh updated to a real universal build:
  `swift build --arch arm64 --arch x86_64` then assemble
  Contents/{MacOS,Info.plist,Resources}. Output moves from
  macos/dist/ → macos/build/ so the sign + DMG scripts can share a
  single location. Static Resources/Info.plist replaces the previous
  inline heredoc. Python runtime embedding (uv venv --relocatable +
  uv pip install backend/) is preserved exactly as it was —
  Contents/Resources/agent-runtime/bin/python3 lives next to the
  binary, so the .app remains end-user-self-contained.
- scripts/sign-and-notarize.sh — codesign with SIGNING_IDENTITY,
  Hardened Runtime + timestamp + entitlements; optional notarytool
  submit + stapler when NOTARY_PROFILE is set. Drops --deep
  (deprecated by Apple), walks the embedded agent-runtime
  explicitly to sign inner dylibs/.so/python3 first. ZIP cleanup
  in an EXIT trap so the build dir doesn't accumulate stale
  artifacts.
- scripts/make-dmg.sh — hdiutil DMG with /Applications symlink for
  the drag-to-install gesture. EXIT trap on the mktemp -d staging
  dir so hdiutil failures don't leak temp files under /var/folders.
  Optional DMG signing when SIGNING_IDENTITY is set.

Out of scope:
- AppIcon (placeholder; add Resources/AppIcon.icns later)
- Sparkle / auto-update — can layer in if needed
- Keyboard shortcuts (⌘R/⌘K/⇧⌘E from earlier M6 draft) — dropped:
  M5 already standardized on click-only affordances after the user
  asked for the shortcuts to be removed, and we shouldn't reintroduce
  them on top.

Release flow:
  cd macos
  ./scripts/build-app.sh
  SIGNING_IDENTITY="Developer ID Application: …" \
  NOTARY_PROFILE="rae-notary" \
    ./scripts/sign-and-notarize.sh
  ./scripts/make-dmg.sh
  # → macos/build/rae.dmg

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kalil0321 kalil0321 force-pushed the claude/proxy-monitor-m6-polish-distribution branch from 17a107c to 68991d8 Compare May 20, 2026 13:57
The "Safari can't connect to the server" failure mode after an
abrupt rae exit (force-quit, kill, crash, kernel panic) was a clean
data loss: the snapshot of the user's pre-rae proxy settings lived
only in `AppState.proxySnapshot` (in-memory). When the process died
without reaching `restoreProxyBeforeExit`, the system proxy stayed
pointing at 127.0.0.1:<our port> with nothing listening — Safari /
Chrome / Atlas all hard-fail on every request — and the user's real
proxy config (Wi-Fi defaults, corporate proxy, VPN settings) was
gone with no way for the next launch to bring it back.

Two complementary fixes:

1. **Persist the snapshot to disk.** Inside applySystemProxy, write
   the snapshot to <Application Support>/ReverseAPI/proxy-snapshot.json
   *before* flipping networksetup, so even a crash between
   `enable()` and the in-memory assignment leaves enough state for
   the next launch to recover. On `restoreSystemProxy` /
   `disableCurrentRaeProxy` / `restoreProxyBeforeExit` we delete the
   sentinel file alongside the in-memory clear.

   AppState.init now also reads the sentinel back into proxySnapshot
   at boot, so `recoverStaleSystemProxyOnLaunch` can do a real
   restore (preserving the user's previous corporate / Wi-Fi proxy
   settings) instead of a blanket disable that dropped everything.
   The recovery message has two variants now: "Restored previous
   proxy settings from a stale session." when the snapshot survived,
   "Recovered stale device proxy from a previous session." when
   only a disable was possible.

   ProxyServiceSnapshot grows a Codable conformance + explicit
   public init (the synthesized memberwise was internal so the
   ReverseAPI target couldn't construct one). The shape stays
   identical.

2. **Catch SIGTERM / SIGINT / SIGHUP.** AppDelegate.installSignalHandlers
   wires DispatchSource.makeSignalSource on the main queue for each
   of the three signals — Activity Monitor's "Quit"/"Force Quit",
   `kill <pid>`, terminal Ctrl-C from `swift run`, and shutdown all
   hit one of these. The handler flips the existing isTerminating
   guard, calls AppLifecycle.shared.restoreProxyBeforeExit()
   synchronously (we have no async budget once the OS decided to
   kill us), then re-raises the signal with SIG_DFL so the process
   exits with the correct termination status.

   SIGKILL (kill -9) and a true crash still bypass everything —
   the kernel doesn't let userspace catch those — but the disk
   sentinel from fix #1 covers that path too: next launch restores
   the snapshot, no manual `networksetup -setwebproxystate Wi-Fi off`
   gymnastics needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

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

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPI/App/AppState.swift Outdated
Comment thread macos/Sources/ReverseAPI/App/AppState.swift Outdated
…e failures

Two open PR review comments on the snapshot persistence work in
4ca2c91:

- AppState.init was reading proxy-snapshot.json unconditionally.
  Cubic flagged that a leftover file from a previous run could be
  loaded after the user has since changed their proxy settings
  manually — when the app later exits via restoreProxyBeforeExit it
  would restore those stale values and overwrite whatever the user
  has now. Gate the load on `systemProxyEnabled` being true (i.e.
  the system proxy currently points at 127.0.0.1:<our port>), which
  is the only state where the on-disk snapshot is actually relevant.
  When that gate fails, delete the file so it can't bite us later.

- applySystemProxy used `try?` when writing the snapshot to disk, so
  a write failure (read-only volume, sandbox refusal, disk full,
  etc.) silently fell through to flipping the system proxy
  anyway — leaving the user with no on-disk record of their
  original settings. If we then crashed, recovery would have nothing
  to restore. Promote the call to `try` so the error propagates and
  the proxy is never enabled without a durable snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

4 issues found across 10 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="macos/Sources/ReverseAPI/UI/ContentView.swift">

<violation number="1" location="macos/Sources/ReverseAPI/UI/ContentView.swift:112">
P2: The sheet binding setter completes onboarding on any dismiss, which bypasses the intended explicit "Get started/Skip" acknowledgment flow.</violation>
</file>

<file name="macos/scripts/sign-and-notarize.sh">

<violation number="1" location="macos/scripts/sign-and-notarize.sh:52">
P2: The nested runtime signing pass is incomplete: it only signs `.dylib`/`.so` files, so other executable code in `agent-runtime` can remain unsigned and break verification/notarization.</violation>
</file>

<file name="macos/Sources/ReverseAPI/App/ReverseAPIApp.swift">

<violation number="1" location="macos/Sources/ReverseAPI/App/ReverseAPIApp.swift:111">
P2: If a signal arrives while `isTerminating` is already true, the handler returns without re-raising the signal, which can leave the process stuck and unresponsive to normal termination signals.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
.sheet(isPresented: Binding(
get: { !hasCompletedOnboarding },
set: { if !$0 { hasCompletedOnboarding = true } }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The sheet binding setter completes onboarding on any dismiss, which bypasses the intended explicit "Get started/Skip" acknowledgment flow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPI/UI/ContentView.swift, line 112:

<comment>The sheet binding setter completes onboarding on any dismiss, which bypasses the intended explicit "Get started/Skip" acknowledgment flow.</comment>

<file context>
@@ -102,6 +107,12 @@ struct ContentView: View {
         }
+        .sheet(isPresented: Binding(
+            get: { !hasCompletedOnboarding },
+            set: { if !$0 { hasCompletedOnboarding = true } }
+        )) {
+            OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
</file context>
Suggested change
set: { if !$0 { hasCompletedOnboarding = true } }
set: { _ in }

codesign --force --options runtime --timestamp \
--sign "$SIGNING_IDENTITY" \
"$bin"
done < <(find "$EMBEDDED_RUNTIME" \( -name "*.dylib" -o -name "*.so" \) -print0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The nested runtime signing pass is incomplete: it only signs .dylib/.so files, so other executable code in agent-runtime can remain unsigned and break verification/notarization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/scripts/sign-and-notarize.sh, line 52:

<comment>The nested runtime signing pass is incomplete: it only signs `.dylib`/`.so` files, so other executable code in `agent-runtime` can remain unsigned and break verification/notarization.</comment>

<file context>
@@ -0,0 +1,90 @@
+        codesign --force --options runtime --timestamp \
+            --sign "$SIGNING_IDENTITY" \
+            "$bin"
+    done < <(find "$EMBEDDED_RUNTIME" \( -name "*.dylib" -o -name "*.so" \) -print0)
+    if [ -x "$EMBEDDED_RUNTIME/bin/python3" ]; then
+        codesign --force --options runtime --timestamp \
</file context>


@MainActor
private func handleSignal(_ sig: Int32) {
guard !isTerminating else { return }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: If a signal arrives while isTerminating is already true, the handler returns without re-raising the signal, which can leave the process stuck and unresponsive to normal termination signals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPI/App/ReverseAPIApp.swift, line 111:

<comment>If a signal arrives while `isTerminating` is already true, the handler returns without re-raising the signal, which can leave the process stuck and unresponsive to normal termination signals.</comment>

<file context>
@@ -77,6 +78,47 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
+
+    @MainActor
+    private func handleSignal(_ sig: Int32) {
+        guard !isTerminating else { return }
+        isTerminating = true
+        // Synchronous restore — we have no async budget once the OS has
</file context>
Suggested change
guard !isTerminating else { return }
guard !isTerminating else {
signal(sig, SIG_DFL)
raise(sig)
return
}

Comment thread macos/Sources/ReverseAPI/App/AppState.swift
Comment on lines +110 to +115
.sheet(isPresented: Binding(
get: { !hasCompletedOnboarding },
set: { if !$0 { hasCompletedOnboarding = true } }
)) {
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Sheet set: closure still marks onboarding complete on Escape

The binding's set: { if !$0 { hasCompletedOnboarding = true } } runs any time SwiftUI transitions isPresented to false, including when the user presses Escape to dismiss the sheet. On macOS this is a natural keyboard shortcut for closing dialogs, so a user who presses Escape without completing any setup step has their onboarding silently flagged as done — contradicting the code comment's stated guarantee ("closing the window before acknowledging doesn't silently mark it complete").

Fix: make set: a no-op and add .interactiveDismissDisabled() to OnboardingView. The explicit button handlers in OnboardingView already set hasCompletedOnboarding = true before calling dismiss(), so the binding's get: will return false on the next render and the sheet will stay closed. Any non-button dismiss path (Escape, window close) will leave hasCompletedOnboarding = false and the sheet will reappear on next launch.

Suggested change
.sheet(isPresented: Binding(
get: { !hasCompletedOnboarding },
set: { if !$0 { hasCompletedOnboarding = true } }
)) {
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
}
.sheet(isPresented: Binding(
get: { !hasCompletedOnboarding },
set: { _ in } // completion is set only by explicit button actions inside OnboardingView
)) {
OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
.interactiveDismissDisabled()
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: macos/Sources/ReverseAPI/UI/ContentView.swift
Line: 110-115

Comment:
**Sheet `set:` closure still marks onboarding complete on Escape**

The binding's `set: { if !$0 { hasCompletedOnboarding = true } }` runs any time SwiftUI transitions `isPresented` to `false`, including when the user presses Escape to dismiss the sheet. On macOS this is a natural keyboard shortcut for closing dialogs, so a user who presses Escape without completing any setup step has their onboarding silently flagged as done — contradicting the code comment's stated guarantee ("closing the window before acknowledging doesn't silently mark it complete").

Fix: make `set:` a no-op and add `.interactiveDismissDisabled()` to `OnboardingView`. The explicit button handlers in `OnboardingView` already set `hasCompletedOnboarding = true` before calling `dismiss()`, so the binding's `get:` will return `false` on the next render and the sheet will stay closed. Any non-button dismiss path (Escape, window close) will leave `hasCompletedOnboarding = false` and the sheet will reappear on next launch.

```suggestion
        .sheet(isPresented: Binding(
            get: { !hasCompletedOnboarding },
            set: { _ in } // completion is set only by explicit button actions inside OnboardingView
        )) {
            OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding)
                .interactiveDismissDisabled()
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Repository owner deleted a comment from kalil-notte May 20, 2026
Repository owner deleted a comment from cubic-dev-ai Bot May 20, 2026
kalil0321 and others added 3 commits May 21, 2026 13:04
…face write failures"

This reverts commit 10e606feea720a9f09b71c408e856ef5533b9a99.
…raunces

Re-align the macOS app's visual identity with the web companion's
design system (see SYSTEM_DESIGN.md). The two products previously
felt unrelated — cold near-black dark-blue palette vs the web's
warm cream/ink + pink magenta brand. Now they share the same dark
mode palette and the wordmark uses the same Fraunces italic that
appears on the landing page.

Scope (intentionally tight):

- Dark mode only — app already forces .preferredColorScheme(.dark);
  light mode is out of scope.
- Fraunces bundled for the wordmark only (~440 KB). Body stays on
  SF Pro, code stays on SF Mono — shipping Inter + JetBrains Mono
  for marginal visual gain would have added ~2 MB for little payoff.
- HTTP method colors stay vivid for semantic discrimination in the
  dense traffic table.

Theme.swift — full color rewrite:
  appBackground   #12131A → #14110e   (--color-cream dark)
  surface         #1B1C1F → #1c1814   (--color-cream-soft dark)
  elevated        #28292E → #262019
  input           #1F2024 → #1a1612   (--color-washed dark)
  textPrimary     #EDEDEF → #fff7f0   (--color-ink dark)
  textSecondary           → cream @ 73%
  textTertiary            → cream @ 55%
  border                  → cream @ 8%   (--color-fd-border dark)
  borderStrong            → cream @ 14%
  accent          #3B82F6 → #ff3d8b   (--color-fd-primary dark, brand pink)
  success         #4CCA84 → #a4d4b8   (--color-mint dark, softened)
  warn            #F08C3A → #d4946e
  danger                    kept (neutral, works in both)
  HTTP method palette       kept (GET blue, POST green, PUT yellow, …)

New tokens:
  brandPink   alias of accent for logo / brand-mark sites
  mint        alias of success for completion semantics
  radiusCard  = 14   radiusInner = 10   radiusInput = 8

The 13 UI files reference Theme tokens by name, so they all auto-
inherit the new palette without per-file edits. Only OnboardingView
needed direct changes (the asterisk + wordmark below).

Fonts.swift (new) — Fraunces italic registration:
- Bundle.module locates the bundled TTF at runtime via the SwiftPM
  resource bundle (Package.swift gets a resources: [.copy("Resources")]
  block on the ReverseAPI target).
- BrandFont.bootstrap() registers the font with Core Text in
  AppDelegate.applicationDidFinishLaunching, before the first window
  appears, so the wordmark doesn't first-paint as SF Italic and
  flip on a later layout pass.
- Font.fraunces(size:weight:) helper. Italic is baked in — Fraunces
  is only shipped here in italic since that's the brand voice.
- Falls back to SF Pro Italic if registration fails (logged).

OnboardingView header:
- Replace SF Symbol "chevron.left.forwardslash.chevron.right" with
  Text("*") in Fraunces 38pt semibold + Theme.brandPink — the brand
  asterisk used in the website header, app icon, hero, marquee.
- "rae" wordmark switched from SF Pro 26pt → Fraunces 30pt italic.
- OnboardingStep.completedGreen now aliases Theme.mint so palette
  tweaks live in one place.

scripts/build-app.sh:
- After the Swift build, copy the SwiftPM resource bundle
  ($BIN_PATH/ReverseAPI_ReverseAPI.bundle) into the .app's
  Contents/Resources/ so Bundle.module finds Fraunces at runtime in
  the distributed .app (not just under `swift run`).

Verification:
- swift build: clean (resource_bundle_accessor.swift auto-generated
  by SwiftPM, Bundle.module resolves).
- swift test: 120/120 pass — no test references Theme color values
  directly.
- Smoke launch via `.build/debug/rae`: no BrandFont registration
  errors logged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

2 issues found across 7 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="macos/Sources/ReverseAPI/UI/Fonts.swift">

<violation number="1" location="macos/Sources/ReverseAPI/UI/Fonts.swift:31">
P3: Use Logger instead of print for diagnostic messages so font-loading issues are visible in Console.app on production builds.</violation>

<violation number="2" location="macos/Sources/ReverseAPI/UI/Fonts.swift:37">
P3: Use Logger instead of print for diagnostic messages so font registration failures are visible in Console.app on production builds.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

var error: Unmanaged<CFError>?
if !CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) {
let message = (error?.takeRetainedValue()).map { String(describing: $0) } ?? "unknown"
print("[BrandFont] failed to register \(name): \(message)")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Use Logger instead of print for diagnostic messages so font registration failures are visible in Console.app on production builds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPI/UI/Fonts.swift, line 37:

<comment>Use Logger instead of print for diagnostic messages so font registration failures are visible in Console.app on production builds.</comment>

<file context>
@@ -0,0 +1,53 @@
+            var error: Unmanaged<CFError>?
+            if !CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) {
+                let message = (error?.takeRetainedValue()).map { String(describing: $0) } ?? "unknown"
+                print("[BrandFont] failed to register \(name): \(message)")
+            }
+        }
</file context>

let url = Bundle.module.url(forResource: name, withExtension: "ttf", subdirectory: "Resources")
?? Bundle.module.url(forResource: name, withExtension: "ttf")
guard let url else {
print("[BrandFont] \(name).ttf not found in bundle — falling back to system font")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Use Logger instead of print for diagnostic messages so font-loading issues are visible in Console.app on production builds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPI/UI/Fonts.swift, line 31:

<comment>Use Logger instead of print for diagnostic messages so font-loading issues are visible in Console.app on production builds.</comment>

<file context>
@@ -0,0 +1,53 @@
+            let url = Bundle.module.url(forResource: name, withExtension: "ttf", subdirectory: "Resources")
+                ?? Bundle.module.url(forResource: name, withExtension: "ttf")
+            guard let url else {
+                print("[BrandFont] \(name).ttf not found in bundle — falling back to system font")
+                continue
+            }
</file context>

…darker dark bg

Response to live feedback on the first retheme pass — the brand
asterisk wasn't rendering as the website's signature petal shape
(missing variable-font axes), the dark background read milky, the
app was locked to dark, and there was no launch moment for the
brand.

Theme.swift — dual-tokenisation:
- New `Color.dynamic(light:dark:)` helper backed by AppKit's
  `NSColor(name:dynamicProvider:)`. Resolves the right value when
  passed to NSWindow.backgroundColor too, not just SwiftUI.
- Every token now has both variants. Light = warm cream/ink
  palette from the website (--color-cream + --color-ink + pink
  #e50d75). Dark = darker cream-dark stack (appBackground deepened
  from #14110e → #0a0806 per user feedback; surface tier still
  warm but unmistakably the "Theme" background).
- HTTP method colors get desaturated dark-mode-only variants that
  also read on cream — single set instead of pure-dark vivid that
  would have vibrated on light.
- New `hex(0xRRGGBB)` helper — RGB-as-fractions math hurt
  readability on every token; hex literals are the canonical
  reference now.

ReverseAPIApp.swift:
- Drop `NSApp.appearance = NSAppearance(named: .darkAqua)` —
  appearance now follows the system setting.
- New `isShowingSplash` gate. SplashView renders for 1.6s on first
  open, then crossfades into the main content via `.task` timer.
  The main hierarchy mounts behind the splash (opacity 0) so first
  paint is instant once the timer fires.
- Refactored window setup into a `mainContent` builder for
  readability now that the body has the splash branch.

ContentView.swift — drop `.preferredColorScheme(.dark)` so the
view inherits the system colorScheme.

SplashView.swift (new):
- Centered `*` brand asterisk + "rae" wordmark in big Fraunces.
- Asterisk pops in with a spring scale + rotation, wordmark slides
  in 250ms after.
- Continuous gentle ±6° wobble on the asterisk so the eye reads it
  as alive — the "small movement" the user asked for, not a full
  spin that would feel toyish.
- Lower caption "WARMING UP" in mono with wide tracking for that
  pre-show texture.

Fonts.swift — Fraunces variable axes:
- `Font.fraunces(size:weight:soft:wonk:opsz:)` now drops to NSFont
  with a variable-axis dictionary (wght, SOFT, WONK, opsz). The
  defaults (`soft=100, wonk=true, opsz=144`) match the website's
  marquee — they're what give the asterisk its rounded petal shape
  vs the generic 6-arm `*` SwiftUI's `.italic()` would have
  produced.
- New `fourCC(_:)` helper that maps "SOFT" / "WONK" / "wght" /
  "opsz" to the UInt32 keys Core Text's
  `kCTFontVariationAttribute` expects.
- Weight signature changed from `Font.Weight` → `CGFloat` (variable
  fonts take a numeric weight axis). Callers updated:
  - OnboardingView wordmark + asterisk (`weight: 600`)
  - TrafficListView "Traffic" header (was SF 13pt → Fraunces 18pt)
  - SessionsListView "Sessions" header (same)

Verification:
- swift build: clean
- swift test: 120/120
- Inspect Onboarding sheet: asterisk now has the signature petal/
  starburst shape from the marquee + reads in pink against the
  deeper dark bg. Wordmark italic. Splash shows on first launch.
  Toggle system dark/light → palette flips.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
kalil0321 and others added 3 commits May 21, 2026 17:10
…re uv

Two follow-up fixes from the first re-theme cut:

1. **Font registration was too late.** Calling BrandFont.bootstrap()
   in `AppDelegate.applicationDidFinishLaunching` runs *after* the
   run loop starts — but `App.init() → body → SplashView` materialise
   their views before that. The first frame resolved
   `Font.fraunces(...)` while Fraunces wasn't yet registered, picking
   up SF Italic, and only flipped to Fraunces on the next layout
   pass. That's the "morph mid-animation" the user saw on the
   splash + onboarding wordmark.

   Move the bootstrap call into `ReverseAPIApp.init()` so the font
   is registered before *any* View body is computed. Leaves a
   pointer comment in AppDelegate so future readers know where it
   moved.

2. **Build failed when HTTPS_PROXY pointed at a dead rae port.**
   uv reads HTTP_PROXY / HTTPS_PROXY / ALL_PROXY (and lowercase
   variants) from the environment. If the shell had those set
   pointing at 127.0.0.1:<rae-port> from a prior session, `uv pip
   install rae-agent` errored with "Connection refused (os error
   61)" the moment rae itself wasn't running on that port. Routing
   the build through a local MITM never made sense anyway — drop
   every proxy variable just before the uv venv + uv pip install
   calls in scripts/build-app.sh.

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

Two targeted fixes from live feedback.

**Composer input lost on the panel.** Theme.input dark was #14110e —
identical to Theme.surface dark, so the agent composer's rounded
input box visually merged with the panel card it sat on. Bump
Theme.input so it's clearly distinct in both modes:
- light: pure white (lifted from #fef8ee cream-soft)
- dark: #211c14 (lifted from #14110e cream-dark)

To keep the small status chips in the action bar from getting too
loud now that `input` is brighter, move CaptureStateChip + CATrustChip
backgrounds from Theme.input → Theme.elevated. Same neutral
"subtle pill" feel, no longer competing with the composer.

**Action bar lacked any brand pull.** The user asked for a touch
of the accent color so the toolbar reads as part of the brand
rather than pure chrome:
- CaptureStateChip recording dot: was `Theme.success` (mint), now
  `Theme.brandPink` with a continuous gentle pulse (1.4× scale +
  55% opacity + soft pink shadow, easeInOut 1.1s repeatForever)
  while `isCapturing`. The recording state is the most "action-
  driven" indicator in the bar, so the brand color lands here.
  Idle stays neutral. `isWorking` shifts from `.yellow` to
  `Theme.warn` for consistency with the rest of the palette.
- SearchButton: hover state was a neutral pill darkening. Now the
  magnifying glass icon flips to `Theme.brandPink`, the
  background becomes `brandPink @ 12%`, and a thin
  `brandPink @ 35%` outline appears. Search is the only
  outbound action in the action bar — pulling the eye to it on
  hover earns its keep.

Markdown UI polish is queued separately (user asked us to track
it for a later session).

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

Follow-up to the earlier composer + accent feedback. The previous
+13% lift on Theme.input (#211c14) was technically distinct from
surface but too subtle to read at a glance; and the send button
still rendered cream-on-cream, so the user never saw the brand
pink anywhere with the agent panel idle.

- Theme.input dark: #211c14 → #2a2418 (~25% lift over surface,
  clearly its own surface tier now).
- Agent composer outer container gains a 1pt `Theme.border` overlay
  on the rounded rect — the input now reads as a focused field
  even in still frames, on either color scheme.
- Send button background was `Theme.textPrimary` (cream/white) when
  enabled. Now `Theme.brandPink` — the single highest-frequency
  action in the agent panel earns the brand color. Icon flips to
  white for contrast (was Theme.appBackground = dark, which read
  fine on cream but loses contrast on pink). Disabled state stays
  on Theme.elevated.

SplashView gains an `init()` that also calls `BrandFont.bootstrap()`
+ a `#Preview` block at the file end. SwiftUI Previews bypass
App.init() (where bootstrap normally runs) so without this the
wordmark would fall back to SF Italic inside Xcode's canvas. The
Core Text registration is idempotent so the double-call is harmless
at runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 21, 2026

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.

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.

1 participant