Repository navigation
feat: capture requests - in-app screenshots, screen recordings and logs - #46
Merged
Merged
Conversation
The Flutter capture path drew each top-level view of the key window at its bounds origin, so every view smaller than the window ended up in the top left corner of the screenshot. Each view is now drawn at its frame in the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eenshots The replay kept up to 60 full-resolution screenshots in memory and never let go of them (about 12 MB each on a 3x iPhone). Each frame is now encoded right away, off the main thread, exactly as it is uploaded (half the point size at 1x, JPEG 0.9) and kept as data, around 15 KB per frame. The upload format to /uploads/sdksteps and the replay payload are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a workflow, an AI agent or a teammate asks the customer for a screenshot or a screen recording, the Messenger sends capture-start and the SDK captures the host app itself (Messenger <-> host bridge, contract sections 7, 8 and 10). - The widget minimizes for the capture: it is dismissed, but its controller, WKWebView and page stay alive and connected, it still counts as open, and widgetOpened / widgetClosed are not called. It is shown again as it was. Closing it while minimized ends the capture and releases the request. - A capture bar in its own window above the app (never key, touches pass through outside the bar, follows the scene's size and orientation, moves above the keyboard, can be dragged to the top, VoiceOver labels), with the texts from capture-start.labels and English fallbacks. - Screenshots: every visible window of the foreground scene in window level order, without the SDK's own windows and the keyboard, drawn with drawViewHierarchyInRect:afterScreenUpdates:NO (Flutter views at their real frame), masked views and secure / one-time code / card number text fields blacked out, JPEG 0.85 with a long edge of at most 2560 px, sent back as capture-image (method frames). Logs for the request follow in the background when attachLogs is set. - Recordings: in-app frame capture behind a recorder protocol (no ReplayKit, no system prompt). Frames are drawn at video size straight into pixel buffers and encoded on a serial queue: H.264 MP4, long edge at most 1280 px, even dimensions fixed at the start (rotation and resizing letterbox), about 1 Mbit/s, a key frame every 2 s, 4 fps adapting between 2 and 8 to keep the main-thread drawing within 30 % of the frame interval, no audio. A red dot, mm:ss / max and Stop while recording; it stops at maxDurationSec and on a memory warning (keeping what was recorded), pauses while the app is inactive (the parts are joined without the gap). A preview with Send / Retake / Cancel, then a multipart upload streamed from a file to /uploads/attachments with progress, /complete with the file's duration and size, and capture-state done once the widget is back. - Background log bundles for logs requests and attachLogs: /claim, the wrapper's flush handler (at most 500 ms), the same collectors and shapes as /bugs/v2, gzipped to /logs, once per request. - New API: setCaptureEnabled:, setRemoteLogCollectionEnabled:, maskView:, unmaskView:, setLogFlushHandler:. - The widget sends capture-capabilities after its ping (frames, no microphone). - While the widget is minimized the app's console output, page views and replay frames count again (they are the app's story), except replay frames during a recording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK websocket now handles capture-request messages (kind logs) in any widget state; update actions are still ignored while the widget is open. The HTTP event ping reads pending logs requests from the answer's cr list, for when the websocket does not deliver them. Both tell the server what the SDK can do: caps on the websocket URL and in the ping body (capture.screenshot, capture.recording, capture.logs; only what is enabled). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Screen recordings and video attachments in the conversation play inline in the widget, as they already do in banners and modals, instead of taking over the screen. Playback still needs a tap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sizes Guards what the server relies on: only plain ids go into request paths, the logs upload is a valid gzip stream of valid JSON that drops network and then console logs when it is too large, the recording's multipart body carries the file unchanged, screenshots and videos stay within their long edge with even video dimensions, letterboxing after a rotation, and the frame rate keeps the main-thread drawing within its budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Screenshots and screen recordings the user shares with support for a capture request (and the chat they belong to) are customer support data, linked to the user's support conversation, used for app functionality only, no tracking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Screenshots and recording frames show the screen mid-animation, but the masks used the model geometry (where an animation ends) and skipped views whose model alpha was 0. A masked view sliding or fading out, or a password field in a moving sheet, showed in the capture. Masks now cover each masked view and secure text field wherever its presentation layer is between right before and right after the window's snapshot (Core Animation freezes presentation values for the whole run loop turn, so a flush starts a fresh transaction around the snapshot), plus its model rect, with 3 pt padding. A view counts as visible when its model or its presentation says so. If the masks cannot be placed, the window is blacked out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of captures These views live in the app's key window, so a banner or modal shown during a capture ended up in the screenshot or recording. While one of them is visible, the window is drawn view by view without them (as Flutter windows already were); otherwise it is still drawn in one go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UIKit refuses a presentation or dismissal while another one runs, without calling its completion. A capture cancelled during the minimize animation counted the still-dismissing widget as restored and left it off screen but "open" (Gleap.open did nothing, the launcher stayed hidden); a close during the restore animation marked it closed while it landed on screen. The widget manager now runs these transitions in order, each once UIKit has finished the one before, with watchdogs for refused ones: open checks that the widget actually landed, minimize only counts once the dismissal finished, a restore waits for a minimize under way, a close dismisses the widget through its presenter (alerts over it go too) also when it is restored or still landing, and waiting transitions a newer one makes pointless are dropped. A capture that ends while the widget goes down brings it back, and Gleap.open shows a widget left minimized without a running capture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erts During the minimize / restore transitions of a capture UIKit refuses to present the page's alert() and its completion handler was never called: WebKit then throws (CompletionHandlerCallChecker) and the app crashed. Closing the widget while an alert was up did the same. The handler now runs exactly once: on OK, right away when the alert cannot be shown (off screen, minimized, on its way in or out, something presented over the widget, or a refused presentation), or when the alert goes away unanswered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A logs request that failed for network reasons (offline, 408, 429, 5xx) was tried again on every later delivery, without end. It now gets three tries over about ten minutes (after 2 and 8 more minutes, deliveries in between are ignored) and is then answered with an event `failed` (reason logs-unreachable-<status>), which ends it on the server instead of it being handed out again. Refused uploads still report `failed` at once; a step that throws ends the attempt rather than leaving the request marked running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS ends web content processes under memory pressure, likely while the widget is minimized for a capture; the widget then stayed blank and the capture image or result sent to it was lost. The widget now loads its page again (right away when on screen, with the loading view; otherwise when it is shown again), and after the page's ping it gets the current request's capture-image and last capture-state once more. A freshly loaded Messenger fetches the request and opens the editor. What the Messenger has finished (capture-done, capture-cancel) is not sent again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recordings and upload bodies of a capture the app did not finish (it was terminated mid-capture) were only removed when the next capture started. At the first initialize the SDK now moves tmp/GleapCapture aside and deletes it (and older leftovers) in the background; no timers or observers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OSLogStore was created lazily into a nonatomic property. A bug report and a logs request read the unified log on different threads at the same time and could both create and assign it. Creation now happens under a lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contract §0.2: a capture failure must never crash the host app. Besides the widget messages and API calls, these now run inside @try/@catch: taps, drags and keyboard notifications of the capture bar and preview, the player's callbacks, the recorder's frame timer, memory warning and scene handlers, the writer and export completions, the upload delegate, network answers, the logs bundle's continuations, and the capture manager's timers and callbacks. An exception in a capture's callback ends that capture as `failed` (released to the server, widget restored); one in another flow is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 1, 2026
Merged
…rk recording preview One look with the web SDK's bar, as asked by Lukas: Bar (screenshot and recording, every state): one fully rounded dark pill (#16161A at 97 % over a dark blur, white 10 % hairline, shadow y 12 / blur 40), sized to its content (at most the screen less 32 pt). A grip of six dots on the leading edge; the bar drags from the grip or anywhere but its buttons, 1:1 with the finger from where it was touched, springs back 8 pt inside the safe area on release, snaps to the horizontal center within 24 pt, and keeps its place (a point in the safe area) for the next captures of the app's run; rotation, resizing and the keyboard re-clamp it. Default: bottom center, 16 pt above the safe area or the keyboard. - Ready: grip · Capture / Start (pill, widget color, else white) · × (circle). The instruction shows as a small caption above (or below) the bar for 4 s or until the first drag or tap; VoiceOver reads it when the bar appears. - Recording: grip · pulsing red dot with mm:ss / max · Stop (red circle). - Working: a spinner in the main button, same size. All circles are 36 pt with 44 pt touch targets. Preview: full screen on #0B0B0C, the video centered with 12 pt rounded corners and a hairline (tap plays or pauses, a play glyph while paused), a thin scrubber under it, × top right, the title as a small caption, and Retake and Send ➤ as pills (full width on iPhone, on the right on iPad). While uploading, Send shows a spinner and the percentage and Retake and the video wait; × still cancels the upload. Every text comes from capture-start.labels (new: barDragHint, previewPlay, previewPause; barStart now "Start"), with English only as a fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
iOS support for capture requests. When a workflow, an AI agent or a teammate asks for a screenshot or a screen recording, the customer answers from the widget card and the SDK captures the app. Logs requests are collected in the background.
Capture, in the app only, with no prompt
drawViewHierarchyInRect).AVAssetWriteras H.264 MP4.maskView:, blacked out before anything leaves the device.mm:ss / maxand a round red Stop.capture-start.labels; English is only a fallback.widgetOpened/widgetClosedcallbacks for that.Logs requests
capture-request) or the ping answer (cr), withcapsannounced on both./logs. Once per request id.setRemoteLogCollectionEnabled:NOalso stops logs attached to captures.failed.Public API (
Gleapclass)Also in this PR
Package.swiftand the podspec linklibz; the podspec adds AVFoundation, CoreMedia and CoreVideo.Verification
pod lib lintpasses with no warnings. The package and the example app build for iPhone and iPad simulators.capture-cancel,Gleap.close()mid-recording, background/foreground and a simulated memory warning all clean up.alert()during a capture: no crash.failed.Not covered
maskView:covers native views). The customer reviews every capture before sending.[CATransaction flush]: the mask fix calls it up to twice per window per frame. It commits pending UI changes slightly earlier in the run loop; no visible effect was seen.Release
Version bump and CHANGELOG at release. The wrappers (React Native, Flutter, Capacitor) need this release; their draft PRs bump the pin.
The whole program: capture requests ("Show me the issue")
Workflows, AI agents (Kai, Kai Resolve, custom agents) and teammates can now ask a customer for a screenshot (with annotation) or a screen recording in the middle of a conversation, and can collect logs (console, network, custom data, environment data, events) from the customer's app in the background.
One server object, the capture request, backs all three. The customer sees a card in the widget; the SDK in their app does the capture; the result lands in the thread and resumes the workflow or wakes the agent. Background log requests travel over the SDK websocket every SDK already keeps open.
How each platform captures:
Design: https://claude.ai/artifact/5i88BeWfcsWUFhhJmPwK5V (private)
PRs in this program (one per repo):
🤖 Generated with Claude Code