M1: Recording Core + UI (Routing Off)#8684
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
|
@codex review |
|
Addressed in #8686 |
…uard, async HUD, picker close (#8686) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
#8688) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
…#8691) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
…d recording (#8692) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
…ng was a regression (#8694) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
| captureWidth = Int(targetDisplay.width) * 2 // Retina | ||
| captureHeight = Int(targetDisplay.height) * 2 |
There was a problem hiding this comment.
🟡 Hardcoded 2x Retina multiplier produces incorrect capture resolution on non-Retina displays
The ScreenRecorder.start() method hardcodes * 2 when computing the capture dimensions from SCDisplay.width/height and SCWindow.frame, assuming all displays are 2x Retina.
Root Cause and Impact
Apple's documentation states that SCDisplay.width and SCDisplay.height return dimensions in points, not pixels. On a 2x Retina display (e.g., built-in MacBook Pro), points × 2 = pixels, so the hardcoded multiplier happens to be correct. However, on a non-Retina external monitor (common with 1080p or 1440p displays connected via HDMI/USB-C), the point dimensions equal the pixel dimensions (1x scale factor).
For a 1920×1080 non-Retina monitor:
targetDisplay.width= 1920 (points = pixels at 1x)- Config is set to 3840×2160 (double the actual pixel resolution)
- ScreenCaptureKit delivers upscaled 3840×2160 frames
- H.264 encoding runs at 4K resolution unnecessarily, wasting CPU/GPU and producing files ~4x larger than needed
The same issue affects window capture at ScreenRecorder.swift:105-106.
The correct approach is to query the backing scale factor for the target display (e.g., via NSScreen.backingScaleFactor) instead of assuming 2x.
Prompt for agents
In clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift, replace the hardcoded `* 2` Retina multiplier on lines 105-106 and 122-123 with a dynamic scale factor lookup. For display capture (lines 122-123), find the matching NSScreen for the SCDisplay and use its backingScaleFactor. For window capture (lines 105-106), similarly resolve the appropriate screen. Example approach:
1. Add a helper function that finds the NSScreen matching an SCDisplay by comparing display IDs: `private static func scaleFactor(for display: SCDisplay) -> Int`
2. For display capture, use `Int(targetDisplay.width) * scaleFactor` instead of `Int(targetDisplay.width) * 2`
3. For window capture, determine which screen the window is on (or default to NSScreen.main) and use its backingScaleFactor
4. Fallback to 2 if no matching screen is found, to preserve current behavior on unknown configurations
Was this helpful? React with 👍 or 👎 to provide feedback.
| guard !state.isActive else { | ||
| log.warning("Cannot start recording — already active (state=\(String(describing: self.state)), owner=\(self.ownerSessionId ?? "nil"))") | ||
| sendStatus(sessionId: sessionId, status: "failed", error: "Another recording is already active") | ||
| return false | ||
| } |
There was a problem hiding this comment.
🟡 sendStatus in single-active guard sends stale attachToConversationId from the existing recording
When RecordingManager.start() is called while another recording is already active, the "already active" guard at line 66 rejects the new request and calls sendStatus to report a failure. However, sendStatus (line 200) reads self.attachToConversationId, which at that point belongs to the currently active recording — not the new request that just failed.
Root Cause
The guard fires before self.attachToConversationId is updated (lines 72-73 only execute after the guard passes). So the recording_status message sent to the daemon for the new rejected session carries the old recording's attachToConversationId.
For example:
- Recording A starts with
attachToConversationId = "conv-A" - Recording B is requested with
attachToConversationId = "conv-B" - Guard fires at line 66,
sendStatusis called at line 68 sendStatusat line 200 readsself.attachToConversationIdwhich is still"conv-A"- Daemon receives a failure status for session B but with
attachToConversationId = "conv-A"
Impact: The daemon receives an incorrect attachToConversationId on the failure status for the rejected recording request, which could cause incorrect state tracking or misattributed failures.
| guard !state.isActive else { | |
| log.warning("Cannot start recording — already active (state=\(String(describing: self.state)), owner=\(self.ownerSessionId ?? "nil"))") | |
| sendStatus(sessionId: sessionId, status: "failed", error: "Another recording is already active") | |
| return false | |
| } | |
| guard !state.isActive else { | |
| log.warning("Cannot start recording — already active (state=\(String(describing: self.state)), owner=\(self.ownerSessionId ?? "nil"))") | |
| sendStatus(sessionId: sessionId, status: "failed", error: "Another recording is already active", overrideConversationId: attachToConversationId) | |
| return false | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| func show(onStart: @escaping (IPCRecordingOptions) -> Void, onCancel: @escaping () -> Void) { | ||
| // Dismiss any existing picker window | ||
| dismiss() | ||
|
|
There was a problem hiding this comment.
🟡 RecordingSourcePickerWindow.dismiss() silently drops pending cancel callback
When RecordingSourcePickerWindow.show() is called while the picker is already open (e.g., a second recording_start arrives with promptForSource: true), the internal dismiss() at line 25 tears down the old window without firing its onCancelCallback. The daemon never receives a failure notification for the first recording request.
Root Cause
dismiss() at RecordingSourcePickerWindow.swift:73-78 sets window?.delegate = nil before closing, which prevents windowWillClose from firing. It also doesn't call fireCancel() itself. Then show() overwrites onCancelCallback at line 27 with the new callback, permanently losing the old one.
Sequence:
recording_startfeat: initialize Next.js app in /web directory #1 arrives withpromptForSource: true→ picker shown,onCancelCallbackset to notify daemon about request feat: initialize Next.js app in /web directory #1recording_startfeat: add platform terraform for GKE deployment #2 arrives withpromptForSource: true→show()callsdismiss()(no cancel fired), then setsonCancelCallbackto notify daemon about request feat: add platform terraform for GKE deployment #2- The daemon never learns that request feat: initialize Next.js app in /web directory #1 was abandoned
Impact: The daemon would continue waiting for a status update on the first recording request indefinitely. This is an edge case requiring two rapid recording_start messages with promptForSource: true.
| func show(onStart: @escaping (IPCRecordingOptions) -> Void, onCancel: @escaping () -> Void) { | |
| // Dismiss any existing picker window | |
| dismiss() | |
| func show(onStart: @escaping (IPCRecordingOptions) -> Void, onCancel: @escaping () -> Void) { | |
| // Fire any pending cancel callback before tearing down the old window | |
| fireCancel() | |
| // Dismiss any existing picker window | |
| dismiss() |
Was this helpful? React with 👍 or 👎 to provide feedback.
| do { | ||
| try await captureStream.startCapture() | ||
| } catch { | ||
| throw RecorderError.streamStartFailed(error.localizedDescription) | ||
| } |
There was a problem hiding this comment.
🟡 ScreenRecorder.start() leaks resources when startCapture() fails — no cleanup of writer, stream, or delegate
When SCStream.startCapture() throws in ScreenRecorder.start(), the method re-throws without calling cleanUpWriter(). This leaves stale references in assetWriter, videoInput, audioInput, stream, and outputDelegate — none of which are cleaned up.
Root Cause
At ScreenRecorder.swift:190-196, all instance state is set before attempting to start the capture:
self.stream = captureStream // line 190
do {
try await captureStream.startCapture() // line 194 — can throw
} catch {
throw RecorderError.streamStartFailed(error.localizedDescription) // line 196 — leaks!
}By this point, self.assetWriter (line 177), self.videoInput (line 161), self.audioInput (line 174), self.outputDelegate (line 181), and self.stream (line 190) are all populated. The thrown error propagates to RecordingManager.start() which catches it, but the ScreenRecorder's internal state remains stale.
Because isRecordingActive is never set to true (that happens at line 199), both stop() and cancelRecording() will early-return on their isRecordingActive guard, leaving these resources stranded until the next successful start() overwrites them.
Impact: Leaked AVAssetWriter, SCStream, and delegate objects persist in memory. The StreamOutputDelegate holds a strong reference back to ScreenRecorder (ScreenRecorder.swift:332), creating a retain cycle that prevents deallocation if the recorder is ever released. In practice the recorder lives as long as the app, so the impact is limited to inconsistent internal state and minor memory waste.
| do { | |
| try await captureStream.startCapture() | |
| } catch { | |
| throw RecorderError.streamStartFailed(error.localizedDescription) | |
| } | |
| do { | |
| try await captureStream.startCapture() | |
| } catch { | |
| cleanUpWriter() | |
| throw RecorderError.streamStartFailed(error.localizedDescription) | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
* feat: add standalone screen recording core, UI, and IPC types Co-Authored-By: Claude <noreply@anthropic.com> * fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: stop stale recorder on start guard, fix forceStop status ordering (#8688) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: only skip stale recorder cancel when another session is confirmed recording (#8692) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
* feat: add standalone screen recording core, UI, and IPC types Co-Authored-By: Claude <noreply@anthropic.com> * fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: stop stale recorder on start guard, fix forceStop status ordering (#8688) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: only skip stale recorder cancel when another session is confirmed recording (#8692) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
* feat: add standalone screen recording core, UI, and IPC types Co-Authored-By: Claude <noreply@anthropic.com> * fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: stop stale recorder on start guard, fix forceStop status ordering (#8688) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: only skip stale recorder cancel when another session is confirmed recording (#8692) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
* M1: Recording Core + UI (Routing Off) (#8684) * feat: add standalone screen recording core, UI, and IPC types Co-Authored-By: Claude <noreply@anthropic.com> * fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: stop stale recorder on start guard, fix forceStop status ordering (#8688) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: only skip stale recorder cancel when another session is confirmed recording (#8692) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * M2: Daemon Recording Control + Deterministic Mapping (#8696) * feat: add daemon recording start/stop handlers with deterministic mapping Co-Authored-By: Claude <noreply@anthropic.com> * fix: prevent map leaks in recording handlers — duplicate start + missing socket (#8697) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * M3: Intent Routing + Strict Classification (Enable Flag) (#8699) * feat: add recording intent detection and standalone routing with feature flag Co-Authored-By: Claude <noreply@anthropic.com> * fix: address M3 PR #8699 review feedback (#8700) * fix: add isStopRecordingOnly guard, sessionId in message_complete, check stop return value Co-Authored-By: Claude <noreply@anthropic.com> * fix: handle polite filler words in isStopRecordingOnly and isRecordingOnly (#8701) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * M4: Finalization + Attachment Delivery (#8703) * feat: add recording finalization and attachment delivery for standalone recordings Co-Authored-By: Claude <noreply@anthropic.com> * fix: address M4 PR #8703 review feedback (#8709) * fix: use file-backed attachment storage, include attachment in message_complete, notify on failure Co-Authored-By: Claude <noreply@anthropic.com> * fix: make file-backed attachments retrievable (#8713) * fix: add file-backed attachment retrieval for recordings Co-Authored-By: Claude <noreply@anthropic.com> * fix: validate Range header bounds and handle suffix ranges (#8716) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * M5: Bundled Recording Skill (Activation Rules Only) (#8717) * feat: add bundled screen-recording skill with activation rules Co-Authored-By: Claude <noreply@anthropic.com> * fix: add macOS OS gate to screen-recording skill (#8718) * fix: restrict screen-recording skill to macOS via os metadata Co-Authored-By: Claude <noreply@anthropic.com> * fix: move os gate inside vellum metadata namespace (#8722) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * M6: Tests + Docs for standalone screen recording (#8727) * feat: add recording intent and handler tests Add comprehensive tests for recording-intent.ts (all 6 exported functions: detectRecordingIntent, isRecordingOnly, detectStopRecordingIntent, stripRecordingIntent, stripStopRecordingIntent, isStopRecordingOnly) and recording handler (start/stop/status flows with proper mocking). Co-Authored-By: Claude <noreply@anthropic.com> * docs: add standalone screen recording section to ARCHITECTURE.md Document the standalone recording lifecycle, key files, IPC messages, intent routing, file-backed attachments, and include a Mermaid sequence diagram showing the recording flow. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: prevent recording ownership overwrite and ensure sessionId on stop (#8828) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: move duplicate-start messaging to callers, return null from handleRecordingStart (#8829) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: type narrowing for nullable handleRecordingStart + add task_routed to else branch (#8830) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: add task_routed to stop-recording path in handleTaskSubmit (#8831) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: add global single-active recording guard and sessionId to stop-recording text delta (#8834) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: resolve handleRecordingStop globally for cross-conversation stop (#8836) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: always create new assistant message for recordings + notify on missing file (#8837) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * refactor: move Recording files and use raw content endpoint - Move Recording*.swift and ScreenRecorder.swift from ComputerUse/ to Recording/ - Switch InlineVideoAttachmentView to fetch raw bytes from /content endpoint instead of base64-decoding JSON, removing intermediate decode step - Stop excluding own app windows from screen capture filter Co-Authored-By: Claude <noreply@anthropic.com> * fix: fetch thumbnail for file-backed attachments - Extract thumbnail from content endpoint for lazy-load attachments that have no inline base64 data Co-Authored-By: Claude <noreply@anthropic.com> * Add microphone capture support to screen recording - Add includeMicrophone option to IPC RecordingOptions contract - Regenerate IPCContractGenerated.swift with new field - Add microphone toggle to RecordingSourcePickerView - Pass includeMicrophone through RecordingManager to ScreenRecorder - Add separate AAC mic track in AVAssetWriter (mono, 64kbps) - Register SCStream microphone output and handle in sample buffer - Check/request microphone permission before recording starts - Gate microphone APIs behind macOS 15 availability checks Co-Authored-By: Claude <noreply@anthropic.com> * fix: address PR review feedback for recording stop edge cases 1. misc.ts: Always call handleRecordingStop even when socket has no prior session binding. Previously, "stop recording" from task_submit with no prior session would skip the stop attempt entirely. 2. recording.ts: Don't clean up deterministic maps when no socket is found. Cleaning up orphaned the client-side recording (still running) while the daemon thought no recording was active, blocking future starts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: harden recording security and cleanup on disconnect 1. Require tracked recording IDs in recording_status — reject unknown session IDs to prevent forged status messages from attaching arbitrary files via the finalization path. 2. Restrict accepted file paths to the recordings directory to prevent path traversal attacks in crafted IPC messages. 3. Clean up recording state on socket disconnect to prevent stale entries from blocking future recordings when a client crashes or disconnects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: clean up recording maps on path rejection and support daemon restart - Extract cleanupMaps() helper to ensure map cleanup runs on all exit paths, including when path validation rejects a file outside the allowed recordings directory (previously skipped by break) - Fall back to attachToConversationId when in-memory maps are missing after daemon restart, preserving recording finalization across restarts - Remove dead else-if branch that was unreachable after early return Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: clean recording state by socket, not session ID, and disable mic on denied - Replace cleanupRecordingForSession(sessionId) with cleanupRecordingsOnDisconnect() which iterates all recording entries instead of a single session ID. Fixes the case where a recording started in conversation A is missed when the socket disconnects while bound to conversation B. - Move cleanup call outside the sessionId check so it runs regardless of session state. - When microphone permission is denied, rebuild options with includeMicrophone=false so recording starts without mic instead of potentially failing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: notify client when recording path fails security validation When a recording_status arrives with a file path outside the allowed recordings directory, send a user-visible error message before cleaning up state, so the user isn't left without feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: scope recording cleanup to disconnecting socket, not global cleanupRecordingsOnDisconnect now accepts the disconnecting socket and a conversation-to-socket lookup so it only clears recordings owned by conversations bound to that socket. This prevents an unrelated socket disconnect from dropping another session's active recording state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: notify client when recording stops without producing a file When recording_status arrives with status 'stopped' but no filePath, send a user-visible message explaining the recording produced no file instead of silently cleaning up. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: route recording notifications via reporting socket Use the socket that delivered recording_status as the primary recipient for completion/failure notifications. This ensures notifications reach the client even when the user has switched to a different conversation since starting the recording. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: regenerate ipc-contract-inventory.json after rebase Add recording_status, recording_start, and recording_stop wire types to the inventory after rebasing onto latest main. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use file_path column for file-backed attachment detection Use getFilePathForAttachment() instead of !dataBase64 to distinguish file-backed attachments from valid zero-byte inline uploads. The file_path column is the authoritative indicator of file-backed storage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add stop-acknowledgement timeout to prevent stale recording state (#9024) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: delete files only for confirmed orphans and suppress non-owner stop status (#9040) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: skip full video download for lazy attachment thumbnails (#9045) Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: replace dead-branch MIME type ternary with lookup map (#9049) * fix: replace dead-branch MIME type ternary with lookup map Co-Authored-By: Claude <noreply@anthropic.com> * fix: use Map for MIME types to avoid prototype pollution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: resolve symlinks in recording file path validation (#9046) * fix: resolve symlinks in recording file path validation Co-Authored-By: Claude <noreply@anthropic.com> * fix: also resolve allowedDir via realpathSync for APFS firmlink compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> * fix: delete orphaned conversation when recording start is rejected (#9048) * fix: delete orphaned conversation when recording start is rejected Co-Authored-By: Claude <noreply@anthropic.com> * fix: keep conversation on rejection, only unbind socket to avoid FK violations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Vellum Assistant <assistant@vellum.ai> Co-authored-by: Claude <noreply@anthropic.com>
Summary
RecordingOptions,RecordingStatus(client-to-server),RecordingStartandRecordingStop(server-to-client)ScreenRecorder— app-agnostic screen recorder using ScreenCaptureKit + AVAssetWriter (H.264 video, optional AAC audio, 30fps)RecordingManager— centralized orchestration with single-active recording guard, state machine, and IPC status reportingRecordingSourcePickerView,RecordingSourcePickerViewModel,RecordingSourcePickerWindow) with display/window selection and audio toggleRecordingHUDWindow) with animated red dot, elapsed timer, and stop buttonrecording_startandrecording_stopIPC callbacks throughDaemonClient→DaemonMessageRouter→AppDelegateforceStop()on app termination to clean up active recordingsRouting is not wired in this milestone. Part of #8672, closes #8673.
Test plan
cd assistant && bunx tsc --noEmit(pre-existing errors only)cd clients/macos && ./build.shIPCRecordingStart,IPCRecordingStop,IPCRecordingStatus,IPCRecordingOptionsexist in generated SwiftServerMessageenum hasrecordingStartandrecordingStopcasesDaemonClienthasonRecordingStartandonRecordingStopcallbacksRecordingManagerenforces single-active guard (returns false if already recording)🤖 Generated with Claude Code