M6: onboarding, app bundle scripts, shortcuts#77
Conversation
There was a problem hiding this comment.
2 issues found across 8 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
This is likely a transient issue. You can re-trigger a run from the dashboard. |
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.
|
@greptile review Generated by Claude Code |
7d880c6 to
4acdba7
Compare
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.
519d22e to
b8a7142
Compare
|
@greptile review Generated by Claude Code |
0f750b6 to
6a1d716
Compare
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.
db29419 to
d7e7ff0
Compare
6a1d716 to
1d8eb1f
Compare
a07e9be to
17a107c
Compare
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>
17a107c to
68991d8
Compare
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>
There was a problem hiding this comment.
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
…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>
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
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 } } |
There was a problem hiding this comment.
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>
| 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) |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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>
| guard !isTerminating else { return } | |
| guard !isTerminating else { | |
| signal(sig, SIG_DFL) | |
| raise(sig) | |
| return | |
| } |
| .sheet(isPresented: Binding( | ||
| get: { !hasCompletedOnboarding }, | ||
| set: { if !$0 { hasCompletedOnboarding = true } } | ||
| )) { | ||
| OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding) | ||
| } |
There was a problem hiding this 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.
| .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.…ace write failures" This reverts commit 142a32e.
…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>
There was a problem hiding this comment.
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)") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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>
…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>
|
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 |
Final stack PR — distribution polish.
What's in
@AppStorage("hasCompletedOnboarding")). Each step has a button that triggers the corresponding action, with live "Installed / Active / Capturing" state.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— universalswift build(arm64 + x86_64), assemblesbuild/ReverseAPI.app/Contents/{MacOS,Info.plist,Resources}.scripts/sign-and-notarize.sh— codesign withSIGNING_IDENTITY, deep verify, optionalNOTARY_PROFILE-drivennotarytool submit --wait+ staple.scripts/make-dmg.sh—hdiutilDMG with/Applicationssymlink, optional resigning of the DMG.Release flow
Drops
build/ReverseAPI.dmg. Upload to a GitHub release and ship.Out of scope
Resources/AppIcon.icns)~/Library/Application Support/ReverseAPI/...-relative orPATH's python3 for now; bundling python-build-standalone is a follow-upGenerated 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
@AppStorage("hasCompletedOnboarding").Distribution
Resources/Info.plist(min macOS 14.0, AppleEvents usage, automatic/sudden termination disabled) andResources/rae.entitlements(Hardened Runtime; no sandbox;network.client,network.server,automation.apple-events).scripts/build-app.sh(universal arm64/x86_64; outputsmacos/build/rae.app; copies staticInfo.plist; copies SwiftPM resource bundleReverseAPI_ReverseAPI.bundlefor fonts; embeds Python atContents/Resources/agent-runtime; installs backend; trims caches; unsets proxy env vars beforeuv).scripts/sign-and-notarize.sh(codesigns with entitlements; explicitly signs nested runtime components; optionalnotarytoolsubmit + staple) andscripts/make-dmg.sh(DMG with/Applicationssymlink; 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.plistand 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.OnboardingViewis a 3-step sheet gated by@AppStorage(\"hasCompletedOnboarding\"); both footer buttons explicitly set the flag before callingdismiss(). However, theset:closure on theisPresentedbinding inContentViewstill fires when the sheet is dismissed via Escape, silently marking onboarding complete without any setup step.applySystemProxynow writes the snapshot to disk before enabling the proxy and deletes it on every clean restore/disable path; SIGINT/SIGTERM/SIGHUP dispatch sources callrestoreProxyBeforeExitbefore re-raising.make-dmg.shcorrectly places the EXIT trap immediately aftermktemp;sign-and-notarize.shdrops deprecated--deepand 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
Prompt To Fix All With AI
Reviews (4): Last reviewed commit: "fix(macos): gate persisted snapshot load..." | Re-trigger Greptile