Pika 2.0.0 - #257
Conversation
Introduces an opt-in "Pro" colour picker: a live loupe that shows the colour, its format, and contrast as you sample. A ColorPickSession abstraction keeps the eyedropper commit path (set / history / undo / overlay / chaining) identical across the system sampler and the custom loupe; picking falls back to the system sampler when Screen Recording permission is unavailable, reverting the preference and notifying once. Consolidates the picker-engine commits from the original custom-color-picker branch, reintegrated onto main's file-split refactor (Eyedropper now lives in its own file; UI strings in PikaText). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesigns the first-run splash into a two-column setup list and adds the Basic/Pro picker tiles to Settings, with Screen Recording permission prompts and a gated "Get started" confirmation. Hides the now-redundant post-pick overlay controls while the custom loupe is active. Consolidates the splash/settings UI commits from the original custom-color-picker branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the checked-in colour-name JSON as the sole source with a downloadable list from the color.pizza API, keeping a bundled copy so names always work offline. - Build: a new "Download Color Names" build phase fetches the default list into Pika/Assets/ColorNames.json before resources are copied, so the shipped app always has an up-to-date offline fallback (best-effort; keeps the committed copy when offline). - Runtime: ColorNamesManager refreshes the catalogue and the selected list on launch, caching lists in Application Support and broadcasting .colorNamesUpdated so the eyedroppers rebuild their lookup. - Settings and the splash gain a shared list chooser (Default selected by default) populated from /v1/lists/. If a chosen list is no longer offered by the API, the selection falls back to Default. - The splash now shows on launch with a pre-selected "Don't show this again" checkbox (hideSplashOnLaunch), which persists on dismissal. - Add the network client entitlement so the sandboxed app can reach the API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZAfuRdMDzHjcJyaSz7unx
Keep the colour-name lists current beyond the launch fetch, and make network state visible instead of silent: - ColorNamesManager now re-checks color.pizza on a 6h timer and on app re-activation (throttled to at most twice an hour), in addition to the existing launch refresh. Offline/API failures still leave the cached and bundled data untouched, so names always resolve. - Track and publish isFetching, lastUpdated (from the cache file's modification date) and lastErrorMessage, and expose a statusDescription. - The shared list picker shows a small spinner while a refresh is in flight and a tooltip with the last-updated time, an in-progress check, or a friendly offline message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZAfuRdMDzHjcJyaSz7unx
…ilableColorNameLists decode key Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ine seed) Removes the two "Download Color Names" build phases (Sparkle + MAS targets). They ran on every build (alwaysOutOfDate = 1) and overwrote the committed Pika/Assets/ColorNames.json in-place with color.pizza's live `default` list — ~10.5MB / 31,914 colours vs the committed ~196KB / 4,959 — a ~50x bundle-size jump plus a perpetually dirty working tree. It's redundant now: ColorNamesManager fetches the full list live at runtime and caches it, and loadColors() reads the bundled JSON as the offline fallback. So the committed ~196KB file stays as a small offline bootstrap for first launch, and the full list loads (and caches) live. Addresses the review's pbxproj:874 warning — option 3 of the three the reviewer laid out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fline-m95nh8 Source colour names from color.pizza with offline fallback
…e, pair-mode setting - PikaWindow: secondary windows track Defaults[.appFloating] instead of a flat .normal, so Preferences/About/Help/Splash don't sit behind the always-on-top main window. System dialogs still sit above .floating, so the permission-prompt fix is preserved. - PikaShortcuts: exclude globally-bound shortcuts from the popover local-monitor match so ⌥⌘D (KeyboardShortcuts.pickPair) no longer double-fires triggerPickContrast. - CustomColorPickSession: detect Display P3 via direct NSColorSpace equality rather than substring-matching localizedName (Adobe RGB (1998) no longer falls through to sRGB). - Consolidate pair-picking onto the pre-existing pickContrastingColor setting and drop the redundant pickMode enum/toggle so the two toggles can't disagree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pFloating About/Help/Preferences are cached after first open and only snapshot the window level at creation, so toggling "float on top" while one is already open left it stranded at the stale level. Fold them into the existing appFloating observer alongside the border/shadow companion windows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splash: - Fit the setup list without scrolling; the runaway "scroll" was the colour-list menu picker fixed-sizing to the longest catalogue name once color.pizza loaded, exploding the row. Replace it with a custom full-width pill Menu that never jumps or collapses. - Add the app icon above "Welcome to Pika" and more top padding. - Colour Picker tiles: dim the Pro mock to match Basic, add a "Recommended" badge and a "Requires Screen Recording" note to Pro, and top-align the two tiles. - Only show the main Pika window after the splash is dismissed. - Add "Show splash" to the app menu (PikaCommands) and the in-window menu. Pro loupe: - Circular magnifier centred on the cursor (system-loupe style) plus a readout card tucked beside it. - Capture the committing click via a full-screen catcher panel so it no longer clicks through to the desktop; the catcher also drives cursor tracking, scroll-to-zoom and right-click cancel, and takes key status for Escape. - Snap the capture region to the device-pixel grid and render the magnified image nearest-neighbour for hard, non-shimmering pixels. - Fix re-arming after an Escape cancel: distinguish a pair-pick re-arm from a fresh pick with an explicit active flag instead of panel state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Capture a native-resolution region and crop the exact centre pixels instead of asking ScreenCaptureKit for a tiny scaled output. The scaled output resampled/blended neighbours, so the magnifier looked soft and the sampled colour drifted with the cursor's sub-pixel position; a pixel-exact crop is crisp and stable (same pixel → same colour). - Escape/zoom/nudge now also work while another app is frontmost: a global key monitor covers it when Pika is trusted for Accessibility, and when it isn't, the pick briefly activates Pika (restoring focus on teardown) so the local monitor handles keys. The catcher takes key status. - Fix the loupe doing nothing on a second pick after an Escape cancel: teardown keeps the panels for reuse, so gate the pair-pick re-arm on an explicit active flag rather than the panels merely existing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cropping a sourceRect capture still looked soft and the sampled colour still drifted: even a nominal 1:1 sourceRect makes ScreenCaptureKit run a scaling pass that blends neighbours. Capture the whole display at its native size (no scaling pass) and crop the pixels around the cursor with CGImage.cropping (a pure pixel op) instead. The magnified pixels are now hard-edged and the sample is the exact device pixel, so re-picking the same pixel yields the same colour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The capture is now pixel-exact, but the loupe looked crisp at some cursor positions and blurred when the cursor sat half a device pixel over. The window was positioned at the cursor's fractional point (NSEvent.mouseLocation is sub-pixel), so the nearest-neighbour magnified image composited at a sub-pixel offset and the compositor anti-aliased it. Snap the circle window's origin to the device-pixel grid so the pixel-art is hard-edged at every cursor position. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Arrow keys nudge the pick one device pixel; holding Shift jumps ten for coarser positioning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Once Screen Recording is live and the Pro picker is chosen, the splash now offers an optional "Grant Accessibility" step (mirroring the Screen Recording one). With it, Escape and arrow-key nudging work while picking over other apps via the global key monitor; without it the picker falls back to briefly activating Pika to receive keys. Takes effect live, no relaunch needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The Accessibility permission prompt is a regular app alert (unlike the Screen Recording system dialog, which sits above floating windows), so the floating splash covered it. Drop the key window to normal level while the prompt is up and restore its floating level when the user returns. - More top padding above the app icon, and grow the window to 720 so the full list still fits without scrolling when a permission step is shown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the single, state-dependent permission CTA with two pills always shown side by side — Screen Recording and Accessibility. Each is a tappable, accent-tinted "grant" pill until allowed, then flips to a non-clickable green pill with a checkmark. A 1.5s poll re-reads the (non-observable) permission status so Accessibility confirms without a relaunch the moment it's allowed; Screen Recording still needs a relaunch to take effect on a first grant, so its pill offers Relaunch after the request and turns green once live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the permission pills not updating on their own: the poll used an inline Timer.publish in .onReceive, which was recreated (and reset) on every re-render before it could fire, so state only updated on a manual re-render (re-clicking a tile). Poll from a .task loop that survives re-renders instead, so enabling/disabling a permission in Settings reflects automatically. - Anchor the setup list to the bottom (.defaultScrollAnchor(.bottom)) so when it's taller than the window it stays scrolled to the end — the last settings and footer stay in view instead of hiding below the fold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The magnified pixels were exact, but the *sampled colour* came from NSBitmapImageRep.colorAt, which reinterprets the pixel through an intermediate device/calibrated colour space. So the reported colour didn't match what was on screen, and re-picking Pika's own rendered swatch walked ~3.4% per round-trip. Read the raw pixel bytes and build the NSColor in the image's exact tagged colour space (the sRGB/Display P3 we captured in). Only the standard ScreenCaptureKit layout (32-bit BGRA, little-endian, alpha-first, opaque) is read directly; any other format falls back to colorAt, so there's no regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Give the circle window enough padding (10 → 18) to contain the drop shadow's blur radius plus its y-offset, so the shadow isn't clipped. - Every fresh pick now captures a frame before showing the loupe (not just the launch's first pick), so it appears already showing the live sample instead of flashing the previous pick's colour. Drops the now-redundant didPrimeConsent fast-path. - Hide the system cursor for the duration of a pick (the loupe circle sits on the cursor), balanced with a show on teardown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restyle the loupe as a camera lens: the magnified glass sits in a dark rim engraved with the live readouts — the colour format curved around the top, the target slot and contrast around the bottom. Adds a CircularText view (glyphs laid out tangent to an arc) and drops the separate readout card and its panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engraving was bold and widely spaced — hard to read and unlike a real lens. Drop to a regular weight, tighten the angular glyph spacing (0.13 → 0.082 rad), and slim the rim (34 → 28) to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `loupeTheme` setting (Settings → Colour Picker, shown for the Pro picker) to switch the loupe's style: - Lens: the rim is filled with the hovered colour and engraved in SF Pro — the format around the top, the closest colour name around the bottom, mixed case, with the text colour adapting to the sample's luminance. - Badge: two white rounded badges hug the inside edge — format on top, slot + contrast on the bottom, monospaced. `CircularText` now advances each glyph by its measured width, so the SF Pro (proportional) lens text spaces evenly. The controller computes the closest colour name per capture for the lens theme (lookup built once per pick). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Tighten the loupe's drop shadow (radius 10 → 5). - Both themes' bottom line is now the slot followed by the colour name (e.g. "Foreground · Cornflower Blue") instead of the contrast ratio; compute the colour name for both themes. - Rotate the badge theme's two badges 45° (top to 1:30, bottom to 7:30). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Show the contrast reading (WCAG ratio or APCA Lc) in both loupe themes'
bottom line during a pair pick, alongside the slot and colour name.
- Permission buttons are now standard prominent buttons (Grant/Relaunch
filled accent, granted filled green and non-interactive) instead of flat
custom pills.
- Add a divider and an intro line ("To enable the Pro picker, you'll need
to grant the following permissions:") above the permission buttons.
- When Screen Recording can't be prompted (already decided/revoked), open
the Screen Recording pane in System Settings so it can be re-enabled,
instead of silently jumping to Relaunch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The granted state is now the flat, knocked-back green outline again (a "flat success") next to the raised prominent action buttons, rather than a second filled button. - Refresh the permission pills when Pika becomes active again (returning from System Settings) and on the system accessibility-changed broadcast, so a granted permission reflects without re-selecting a tile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use a regular bordered button for the grant/relaunch actions instead of a filled prominent one, so it reads as a standard button next to the flat green granted state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim the flat green "granted" box min height (34 → 29) so it lines up with the bordered grant button next to it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pick flow sourced the comparison colour via `NSApp.delegate as? AppDelegate`, which is always nil under @NSApplicationDelegateAdaptor (it's SwiftUI's forwarding wrapper). So the loupe never got the other slot's colour and the contrast ratio never appeared. Use `AppDelegate.shared`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The glass outline (both themes) is now black in light mode, white in dark mode. - The badge theme's badges are white with dark text in light mode, black with light text in dark mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
🐑 shepherd (automated) — status update Handled round-18 review (claude, "Approved with comments", 9f985c4):
|
|
🐑 shepherd (automated) — status update Working through round 18's Code Quality comments (review approved overall; both items are 🟡/ℹ️, non-blocking):
No code changes made this round. |
|
🐑 shepherd (automated) — status update What cleared: CI green (Unit Tests + |
|
🐑 shepherd (automated) — correction My previous "ready to merge" comment was premature — retracting it. The ClickShield hit-test order decision is still open and unanswered: no manual click-to-edit / click-to-pick verification has been confirmed, and I can't drive the running app from here to check it myself. This PR is not ready to merge until that's confirmed (👍 the decision comment, or reply with your call). |
There was a problem hiding this comment.
🔴 Changes requested
Eighteenth round. Since round 17's "ready to merge" (aedb0b9), 41 new commits landed a value-scrub feature: click-drag and two-finger-scroll to nudge a field's value directly, a floating live-preview pill, frozenSize width budgeting so the row doesn't reflow mid-scrub, Escape-to-cancel, and Cmd-Z/Cmd-Shift-Z routing to the focused field's own undo manager. ScrubTextField.swift (544 lines) was also split out of EditableColorValue.swift into its own file.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🔴 Blocked |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. No new input-handling or trust-boundary surface — this round is UI interaction and layout.
🧹 Code Quality — 🔴 Blocked
- ⛔ [BLOCKING]
Pika/Views/ScrubTextField.swift:176andPika/Views/EditableColorValue.swift:751— a scrub that pushes a component out of the sRGB gamut can commit a colour different from the one it just previewed live. See inline comments for the repro. Root cause:valuesis only ever patched for the dragged index at session end (viatext = ...inonDragEnd/handleScrollEnded); the other components stay atsessionStartValuesfor the whole session (previewLiveScrub,EditableColorValue.swift:447-485, only reads them, never writes them back).finishEditing(EditableColorValue.swift:507-513) then recomposes from that mixed array instead of the colour that was actually just on screen. - ℹ️ [INFO]
Pika/Views/ScrubTextField.swift:1994-1999(thecase .keyDownbranch inmouseDown's tracking loop) — every non-Escape keydown while the mouse button is down on a draggable field is dequeued and dropped, not just during a scrub. The code comment already calls this out and accepts it ("narrow window: only while the mouse button is held on this field") — worth knowing the window is the full drag duration, not just the pre-threshold click-vs-drag decision, since a scrub can stay open as long as the user keeps moving the mouse.
⚡ Performance — 🟢 Approved
Nothing applicable. The new ReadoutHeightKey GeometryReader adds one more preference-driven measurement per swatch, on par with the existing adaptive-layout GeometryReader; FlowLayout stays O(n) over its small fixed fragment count.
📝 Docs — 🟢 Approved
The new doc comments on frozenSize, sessionStartValues, rowScrubPreview, and the worstCaseComponentString/boundedWorstCaseJoined budgeting are specific about the failure modes they're routing around, consistent with this file's established standard.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run.
There was a problem hiding this comment.
🔴 Changes requested
Eighteenth round. Since round 17's "ready to merge" (aedb0b9), 41 new commits landed a value-scrub feature: click-drag and two-finger-scroll to nudge a field's value directly, a floating live-preview pill, frozenSize width budgeting so the row doesn't reflow mid-scrub, Escape-to-cancel, and Cmd-Z/Cmd-Shift-Z routing to the focused field's own undo manager. ScrubTextField.swift (544 lines) was also split out of EditableColorValue.swift into its own file.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🔴 Blocked |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. No new input-handling or trust-boundary surface — this round is UI interaction and layout.
🧹 Code Quality — 🔴 Blocked
- ⛔ [BLOCKING]
Pika/Views/ScrubTextField.swift:176andPika/Views/EditableColorValue.swift:751— a scrub that pushes a component out of the sRGB gamut can commit a colour different from the one it just previewed live. See inline comments for the repro. Root cause:valuesis only ever patched for the dragged index at session end (viatext = ...inonDragEnd/handleScrollEnded); the other components stay atsessionStartValuesfor the whole session (previewLiveScrub,EditableColorValue.swift:447-485, only reads them, never writes them back).finishEditing(EditableColorValue.swift:507-513) then recomposes from that mixed array instead of the colour that was actually just on screen. - ℹ️ [INFO]
Pika/Views/ScrubTextField.swift:1994-1999(thecase .keyDownbranch inmouseDown's tracking loop) — every non-Escape keydown while the mouse button is down on a draggable field is dequeued and dropped, not just during a scrub. The code comment already calls this out and accepts it ("narrow window: only while the mouse button is held on this field") — worth knowing the window is the full drag duration, not just the pre-threshold click-vs-drag decision, since a scrub can stay open as long as the user keeps moving the mouse.
⚡ Performance — 🟢 Approved
Nothing applicable. The new ReadoutHeightKey GeometryReader adds one more preference-driven measurement per swatch, on par with the existing adaptive-layout GeometryReader; FlowLayout stays O(n) over its small fixed fragment count.
📝 Docs — 🟢 Approved
The new doc comments on frozenSize, sessionStartValues, rowScrubPreview, and the worstCaseComponentString/boundedWorstCaseJoined budgeting are specific about the failure modes they're routing around, consistent with this file's established standard.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run.
…le values On scrub end (click-drag or scroll), finishEditing recomposed the committed colour from the values array. During an out-of-gamut scrub, recompose clamps across components — but only the dragged component's achieved value was written back to values; the others stayed at their session-start values. The commit therefore paired the dragged component's new value with the others' stale ones and landed on a third colour the user never saw (and saved that to history). previewLiveScrub already sets eyedropper.color to the correct clamped colour on every frame, so finishDragOrScrollSession now commits that live colour directly (finishEditing(commitLiveScrubColor:)) rather than re-recomposing. The typed-edit path is unchanged. Repro: oklch(40.68% 0.2173 264.58), drag chroma to 1.0 — live preview and commit now match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔴 Changes requested
Eighteenth round. Since the last review (aedb0b9), 45 commits landed a scrub-interaction overhaul: click-drag/scroll-to-scrub now show a live floating pill instead of updating the field inline, recompose gamut-aware and reversibly from the session's pristine starting values (sessionStartValues/previewLiveScrub), and freeze the row's font/width for the gesture to stop wrap flicker. The AppKit-backed field implementation (ScrubbableColorField/ScrubTextField) moved out of EditableColorValue.swift into its own new Pika/Views/ScrubTextField.swift. Also new: Cmd-Z/Cmd-Shift-Z now actually dispatch in window mode (PikaApp.swift/AppDelegate.swift), programmatic field writes route through the field editor so they're undoable, and a version bump to 2.0.0-beta2 (build 100).
The branch's very last commit (0a768f3) fixed a real bug where finishEditing recomposed the scrub's committed colour from values instead of the live, gamut-clamped colour already on screen. Reading the full diff, that fix is undone by an earlier commit in the same round via a different path — see the blocking finding below.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🔴 Blocked |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is view/gesture logic, an undo-manager lookup, and menu wiring — no new input-handling or trust-boundary surface.
🧹 Code Quality — 🔴 Blocked
- ⛔ [BLOCKING]
Pika/Views/ScrubTextField.swift:176andPika/Views/EditableColorValue.swift:762— inline comments. Settingtextat the end of a click-drag or scroll scrub (to unfreeze the field's display) re-triggers the SwiftUI binding'ssetclosure, which recomposeseyedropper.colorfrom stale, non-dragged component values — reopening the exact bug0a768f3(this round's last commit) fixed for the directfinishEditingpath. The swatch visibly jumps to a different colour at mouse-up for any scrub that crossed the sRGB gamut boundary, and that wrong colour is what lands in pick history. - 💡 [SUGGESTION]
Pika/Views/ScrubTextField.swift:438— inline comment. The drag-tracking loop now dequeues.keyDown(new this round) but discards every key except Escape, including before the drag threshold is reached — a behaviour change from before, where such a keypress stayed queued and dispatched normally aftermouseUp.
⚡ Performance — 🟢 Approved
Nothing new. previewLiveScrub's per-frame decompose/recompose runs at drag/scroll cadence, same order of work the pre-existing per-frame recompose already did; the frozen-font/pill approach avoids adding a new per-frame layout pass.
📝 Docs — 🟢 Approved
The new ScrubTextField.swift carries the same standard of doc comments as the rest of this feature (each AppKit workaround explains the specific failure mode it routes around, e.g. the trackpad-momentum .ended handling, the becomeFirstResponder mouse-down refusal). Consistent with this file's established bar from earlier rounds.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
The round-9 EyedropperButton.swift:12 VoiceOver/NSAccessibility question and the round-15 horizontalInset duplication suggestion are both still unanswered/unchanged, but neither refreshes round over round per this review's own rules, so they're not repeated here.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run.
There was a problem hiding this comment.
🔴 Changes requested
Eighteenth round. Since the last review (aedb0b9), 45 commits landed a scrub-interaction overhaul: click-drag/scroll-to-scrub now show a live floating pill instead of updating the field inline, recompose gamut-aware and reversibly from the session's pristine starting values (sessionStartValues/previewLiveScrub), and freeze the row's font/width for the gesture to stop wrap flicker. The AppKit-backed field implementation (ScrubbableColorField/ScrubTextField) moved out of EditableColorValue.swift into its own new Pika/Views/ScrubTextField.swift. Also new: Cmd-Z/Cmd-Shift-Z now actually dispatch in window mode (PikaApp.swift/AppDelegate.swift), programmatic field writes route through the field editor so they're undoable, and a version bump to 2.0.0-beta2 (build 100).
The branch's very last commit (0a768f3) fixed a real bug where finishEditing recomposed the scrub's committed colour from values instead of the live, gamut-clamped colour already on screen. Reading the full diff, that fix is undone by an earlier commit in the same round via a different path — see the blocking finding below.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🔴 Blocked |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is view/gesture logic, an undo-manager lookup, and menu wiring — no new input-handling or trust-boundary surface.
🧹 Code Quality — 🔴 Blocked
- ⛔ [BLOCKING]
Pika/Views/ScrubTextField.swift:176andPika/Views/EditableColorValue.swift:762— inline comments. Settingtextat the end of a click-drag or scroll scrub (to unfreeze the field's display) re-triggers the SwiftUI binding'ssetclosure, which recomposeseyedropper.colorfrom stale, non-dragged component values — reopening the exact bug0a768f3(this round's last commit) fixed for the directfinishEditingpath. The swatch visibly jumps to a different colour at mouse-up for any scrub that crossed the sRGB gamut boundary, and that wrong colour is what lands in pick history. - 💡 [SUGGESTION]
Pika/Views/ScrubTextField.swift:438— inline comment. The drag-tracking loop now dequeues.keyDown(new this round) but discards every key except Escape, including before the drag threshold is reached — a behaviour change from before, where such a keypress stayed queued and dispatched normally aftermouseUp.
⚡ Performance — 🟢 Approved
Nothing new. previewLiveScrub's per-frame decompose/recompose runs at drag/scroll cadence, same order of work the pre-existing per-frame recompose already did; the frozen-font/pill approach avoids adding a new per-frame layout pass.
📝 Docs — 🟢 Approved
The new ScrubTextField.swift carries the same standard of doc comments as the rest of this feature (each AppKit workaround explains the specific failure mode it routes around, e.g. the trackpad-momentum .ended handling, the becomeFirstResponder mouse-down refusal). Consistent with this file's established bar from earlier rounds.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
The round-9 EyedropperButton.swift:12 VoiceOver/NSAccessibility question and the round-15 horizontalInset duplication suggestion are both still unanswered/unchanged, but neither refreshes round over round per this review's own rules, so they're not repeated here.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run.
Code review skippedThe review step finished without leaving a comment, and this PR doesn't |
…fires the binding Guard binding(for:layout:)'s set closure with !isScrubbing: onDragEnd/ handleScrollEnded write text once at scrub end purely to unfreeze the field's display, but that write re-triggers previewIfValid, which recomposes eyedropper.color from the still-stale, non-dragged component values and overwrites the correct live colour right before finishEditing(commitLiveScrubColor: true) commits it - reopening the bug 0a768f3 just fixed via a different path.
|
🐑 shepherd (automated) — responding to round 18's review
All three inline threads resolved. |
There was a problem hiding this comment.
🟡 Approved with comments
Eighteenth round. Since the last review (aedb0b9), 51 commits landed the big one this branch has been building toward: click-drag/scroll/arrow-key scrubbing on every colour field, with a floating pill preview and a new ScrubTextField.swift (544 lines) backing it, plus Cmd-Z/Cmd-Shift-Z wired to actually dispatch in window mode, a Settings-chrome fix, and the 2.0.0-beta2 (build 100) version bump. This is dense, and it visibly iterated within this same round — several "Address review feedback" commits and two late fixes (0a768f3, 78a71e8) each chase a specific reproducible bug (chroma dragged past the sRGB gamut boundary committing a colour nobody saw) to ground, with a regression test (test_scrubFromPristineValues_isReversible) added for the fix. Read all changed files in full and traced the scrub commit path end to end: previewLiveScrub sets eyedropper.color live and correctly every frame from sessionStartValues (never the ratcheting live values — verified reversibility), and finishDragOrScrollSession now commits that already-correct colour directly (commitLiveScrubColor: true) rather than re-recomposing from the stale values array; the scrub-end text write that re-triggered the same bug through a second path is guarded off with !isScrubbing in binding(for:layout:)'s setter. Both fixes read correct against the current code.
CI's Unit Tests pass on 78a71e8.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🟡 Comments |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is local UI interaction (drag/scroll/keyboard scrubbing, an undo-manager lookup scoped to the key window's first responder) and a version bump — no new input-handling or trust-boundary surface.
🧹 Code Quality — 🟡 Comments
- ℹ️
Pika/Views/ScrubTextField.swift:116-117— inline comment.ScrubbableColorField.makeCoordinator()capturesonSubmit/onCancelonce; unliketextandonStep,updateNSViewnever refreshes them on theCoordinatorafterward, so a Return/Escape on a field whoseNSViewidentity has persisted across aformat/style/colorSpaceswitch would runcommitEditing/revertEditingclosed over the old value of those. Traced whether that's reachable: it isn't —handleFormatOrStyleChange()(wired to.onChange(of: format/style/colorSpace)) unconditionally aborts any open edit session on exactly those changes, and that abort clearsfocusedIndex/blurs the field before a staleonSubmit/onCancelcould ever fire. Worth confirming that guard stays in place if this code is touched again — it's the only thing keeping the asymmetry withonStep(which the same method does refresh) harmless.
Everything else already flagged in this file's history is unchanged and not repeated: the round-9 EyedropperButton.swift:12 VoiceOver/NSAccessibility ❓ and the round-15 horizontalInset duplication 💡 are both still open, non-refreshing.
⚡ Performance — 🟢 Approved
Nothing. ScrollValueAdapter's local scroll-wheel monitor and ScrubTextField.mouseDown's blocking event-tracking loop are both scoped to their own field's lifetime/gesture, matching the established HorizontalScrollWheelAdapter pattern; the new ReadoutHeightKey/GeometryReader background in EyedropperButton.swift measures the padded VStack's own natural size (attached before the .frame(height:) that imposes the shared height), so it doesn't create the same measure/impose feedback loop round 15-17 spent three commits chasing down for swatch width.
📝 Docs — 🟢 Approved
Doc comments throughout ScrubTextField.swift/EditableColorValue.swift continue this file's standard of explaining the specific failure mode being routed around rather than restating the code; the two now-stale EditableColorValue.clampValuesToRange cross-references (renamed to finalizeValues back in round 9) are fixed in Pika/Services/ColorDecomposition.swift and both test files.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run.
There was a problem hiding this comment.
🟡 Approved with comments
Eighteenth round. Since the last review (aedb0b9), 51 commits landed the big one this branch has been building toward: click-drag/scroll/arrow-key scrubbing on every colour field, with a floating pill preview and a new ScrubTextField.swift (544 lines) backing it, plus Cmd-Z/Cmd-Shift-Z wired to actually dispatch in window mode, a Settings-chrome fix, and the 2.0.0-beta2 (build 100) version bump. This is dense, and it visibly iterated within this same round — several "Address review feedback" commits and two late fixes (0a768f3, 78a71e8) each chase a specific reproducible bug (chroma dragged past the sRGB gamut boundary committing a colour nobody saw) to ground, with a regression test (test_scrubFromPristineValues_isReversible) added for the fix. Read all changed files in full and traced the scrub commit path end to end: previewLiveScrub sets eyedropper.color live and correctly every frame from sessionStartValues (never the ratcheting live values — verified reversibility), and finishDragOrScrollSession now commits that already-correct colour directly (commitLiveScrubColor: true) rather than re-recomposing from the stale values array; the scrub-end text write that re-triggered the same bug through a second path is guarded off with !isScrubbing in binding(for:layout:)'s setter. Both fixes read correct against the current code.
CI's Unit Tests pass on 78a71e8.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🟡 Comments |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is local UI interaction (drag/scroll/keyboard scrubbing, an undo-manager lookup scoped to the key window's first responder) and a version bump — no new input-handling or trust-boundary surface.
🧹 Code Quality — 🟡 Comments
- ℹ️
Pika/Views/ScrubTextField.swift:116-117— inline comment.ScrubbableColorField.makeCoordinator()capturesonSubmit/onCancelonce; unliketextandonStep,updateNSViewnever refreshes them on theCoordinatorafterward, so a Return/Escape on a field whoseNSViewidentity has persisted across aformat/style/colorSpaceswitch would runcommitEditing/revertEditingclosed over the old value of those. Traced whether that's reachable: it isn't —handleFormatOrStyleChange()(wired to.onChange(of: format/style/colorSpace)) unconditionally aborts any open edit session on exactly those changes, and that abort clearsfocusedIndex/blurs the field before a staleonSubmit/onCancelcould ever fire. Worth confirming that guard stays in place if this code is touched again — it's the only thing keeping the asymmetry withonStep(which the same method does refresh) harmless.
Everything else already flagged in this file's history is unchanged and not repeated: the round-9 EyedropperButton.swift:12 VoiceOver/NSAccessibility ❓ and the round-15 horizontalInset duplication 💡 are both still open, non-refreshing.
⚡ Performance — 🟢 Approved
Nothing. ScrollValueAdapter's local scroll-wheel monitor and ScrubTextField.mouseDown's blocking event-tracking loop are both scoped to their own field's lifetime/gesture, matching the established HorizontalScrollWheelAdapter pattern; the new ReadoutHeightKey/GeometryReader background in EyedropperButton.swift measures the padded VStack's own natural size (attached before the .frame(height:) that imposes the shared height), so it doesn't create the same measure/impose feedback loop round 15-17 spent three commits chasing down for swatch width.
📝 Docs — 🟢 Approved
Doc comments throughout ScrubTextField.swift/EditableColorValue.swift continue this file's standard of explaining the specific failure mode being routed around rather than restating the code; the two now-stale EditableColorValue.clampValuesToRange cross-references (renamed to finalizeValues back in round 9) are fixed in Pika/Services/ColorDecomposition.swift and both test files.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run.
|
🐑 shepherd (automated) — round 18 review response Round 18 review (claude[bot] / github-actions[bot], "Approved with comments") raised one Code Quality note, no other action items:
The review's two re-mentions of already-settled items (round-9 EyedropperButton VoiceOver, round-15 No blocking findings this round. |
There was a problem hiding this comment.
🟡 Approved with comments
Eighteenth round. Since the last review (aedb0b9), the branch merged 30+ commits building the full click-drag/scroll-to-scrub value editing feature, undo/redo wiring, and a handful of layout fixes — the biggest single round on this PR. Diff since aedb0b9: Pika/Views/EditableColorValue.swift (+803/-590 across the file), a new Pika/Views/ScrubTextField.swift (544 lines: the custom NSTextField that owns its own mouseDown to resolve click-vs-drag before anything focuses), plus EyedropperButton.swift, EyedropperItem.swift, ColorPickers.swift, ContentView.swift, AppDelegate.swift, PikaApp.swift, WindowCoordinator.swift, SplashView.swift, FlowLayout.swift, and new tests in EditableColorValueTests.swift. Read every changed file in full plus the full diff since aedb0b9.
The scrub-reversibility invariant (drag out of gamut and back returns the exact starting colour) is unit-tested (PikaTests/EditableColorValueTests.swift:34-68, test_scrubFromPristineValues_isReversible) and I traced the mechanism by hand: previewLiveScrub (EditableColorValue.swift:455) always recomposes from sessionStartValues, never from the live clamped values, which is exactly what makes a drag path-independent. The doc comments throughout ScrubTextField.swift and EditableColorValue.swift are unusually good — several explain a specific AppKit gotcha discovered by trial (the shared-field-editor pre-focus dance, .eventTracking mode owning mouseDown, the field-editor/cell sub-pixel alignment mismatch) rather than just narrating the code. No force-unwraps, try!, or empty catches introduced (languages/swift.md); the two new local event monitors and closures that capture nsView/self all use [weak ...] correctly.
I could not run the app or the test suite in this environment — reviewed by reading only, dependencies aren't installed so I can't confirm the two questions below either way.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🟡 Comments |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is entirely local UI/editing state — typed colour values, drag/scroll gesture math, undo routing — no network or trust-boundary surface.
🧹 Code Quality — 🟡 Comments
- ❓
Pika/AppDelegate.swift:332-353—focusedFieldUndoManagerreads(NSApp.keyWindow?.firstResponder as? NSTextView)?.undoManagerand routes Cmd-Z/Cmd-Shift-Z there first (new this round, alongsidePikaApp.swift's replaced.undoRedocommand group andScrubTextField.setText(ScrubTextField.swift:311), which now routes programmatic value writes througheditor.insertTextspecifically so they register on that undo manager). AppKit's field editor is a single instance shared by everyNSTextFieldin a window and reused across whichever control is currently being edited; itsundoManageris a property of that shared editor instance, not scoped per-control. If that's accurate, tabbing from one colour field to another mid-edit without typing in the new field, then pressing Cmd-Z, could apply an undo step recorded for the previous field's text against the new field's now-different string — at best a value that doesn't match what was undone, at worst an out-of-rangeNSRangegiven the string lengths differ. I can't verify AppKit's actual runtime behaviour here by reading alone (same category of question as the round 9EyedropperButton.swift:12VoiceOver one — might already be handled byNSTextField's own re-configuration of the shared editor when focus moves, which I can't see from source). Worth confirming by hand: type in one field, Tab to a second field without typing, Cmd-Z. - ❓
Pika/Views/SplashView.swift:334(permissionArea.id(permissionTick)) — new this round, fixing a real stale-pill bug (permissionTick, bumped by a 1-second poll atSplashView.swift:348-353, wasn't otherwise establishing a SwiftUI dependency sincehasPermission/hasAccessibilityread non-observable system APIs)..id()forces SwiftUI to tear down and recreate the whole subtree — including the "Grant Screen Recording…"/"Grant Accessibility…"Buttons (SplashView.swift:405-421) — on every tick. If a click's mouseDown/mouseUp pair straddles one of those once-a-second identity resets, the button's action plausibly doesn't fire, since the view instance that captured the mouseDown is gone by the time the reset happens. This is the exact flow that unblocks the Pro picker, so an intermittently-swallowed click there is high-friction if it reproduces — but I can't confirm SwiftUI's precise behaviour under.id()churn without running it, so flagging as a question rather than asserting it.
⚡ Performance — 🟢 Approved
Nothing. previewLiveScrub/fontSize(for:) are cheap pure work run once per drag/scroll frame; the new ScrollValueAdapter local scroll monitors (one per draggable component field) and the ReadoutHeightKey/ColorPickers.swift:32-48 preference round-trip follow the same bounded, already-reviewed patterns as this file's earlier GeometryReader measurement passes.
📝 Docs — 🟢 Approved
The two stale cross-file references to the renamed clampValuesToRange (now finalizeValues) were caught and fixed within this same round (ea539b906, touching ColorDecomposition.swift:32,266 and WindowCoordinator.swift:350); grepped the whole tree for any I could still find — none remain.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
The round-9 EyedropperButton.swift:12 VoiceOver/NSAccessibility ❓ is still open and unchanged; not repeated here per this review's own rules for non-refreshing findings. Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker/test suite were not run.
There was a problem hiding this comment.
🟡 Approved with comments
Eighteenth round. Since the last review (aedb0b9), the branch merged 30+ commits building the full click-drag/scroll-to-scrub value editing feature, undo/redo wiring, and a handful of layout fixes — the biggest single round on this PR. Diff since aedb0b9: Pika/Views/EditableColorValue.swift (+803/-590 across the file), a new Pika/Views/ScrubTextField.swift (544 lines: the custom NSTextField that owns its own mouseDown to resolve click-vs-drag before anything focuses), plus EyedropperButton.swift, EyedropperItem.swift, ColorPickers.swift, ContentView.swift, AppDelegate.swift, PikaApp.swift, WindowCoordinator.swift, SplashView.swift, FlowLayout.swift, and new tests in EditableColorValueTests.swift. Read every changed file in full plus the full diff since aedb0b9.
The scrub-reversibility invariant (drag out of gamut and back returns the exact starting colour) is unit-tested (PikaTests/EditableColorValueTests.swift:34-68, test_scrubFromPristineValues_isReversible) and I traced the mechanism by hand: previewLiveScrub (EditableColorValue.swift:455) always recomposes from sessionStartValues, never from the live clamped values, which is exactly what makes a drag path-independent. The doc comments throughout ScrubTextField.swift and EditableColorValue.swift are unusually good — several explain a specific AppKit gotcha discovered by trial (the shared-field-editor pre-focus dance, .eventTracking mode owning mouseDown, the field-editor/cell sub-pixel alignment mismatch) rather than just narrating the code. No force-unwraps, try!, or empty catches introduced (languages/swift.md); the two new local event monitors and closures that capture nsView/self all use [weak ...] correctly.
I could not run the app or the test suite in this environment — reviewed by reading only, dependencies aren't installed so I can't confirm the two questions below either way.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🟡 Comments |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is entirely local UI/editing state — typed colour values, drag/scroll gesture math, undo routing — no network or trust-boundary surface.
🧹 Code Quality — 🟡 Comments
- ❓
Pika/AppDelegate.swift:332-353—focusedFieldUndoManagerreads(NSApp.keyWindow?.firstResponder as? NSTextView)?.undoManagerand routes Cmd-Z/Cmd-Shift-Z there first (new this round, alongsidePikaApp.swift's replaced.undoRedocommand group andScrubTextField.setText(ScrubTextField.swift:311), which now routes programmatic value writes througheditor.insertTextspecifically so they register on that undo manager). AppKit's field editor is a single instance shared by everyNSTextFieldin a window and reused across whichever control is currently being edited; itsundoManageris a property of that shared editor instance, not scoped per-control. If that's accurate, tabbing from one colour field to another mid-edit without typing in the new field, then pressing Cmd-Z, could apply an undo step recorded for the previous field's text against the new field's now-different string — at best a value that doesn't match what was undone, at worst an out-of-rangeNSRangegiven the string lengths differ. I can't verify AppKit's actual runtime behaviour here by reading alone (same category of question as the round 9EyedropperButton.swift:12VoiceOver one — might already be handled byNSTextField's own re-configuration of the shared editor when focus moves, which I can't see from source). Worth confirming by hand: type in one field, Tab to a second field without typing, Cmd-Z. - ❓
Pika/Views/SplashView.swift:334(permissionArea.id(permissionTick)) — new this round, fixing a real stale-pill bug (permissionTick, bumped by a 1-second poll atSplashView.swift:348-353, wasn't otherwise establishing a SwiftUI dependency sincehasPermission/hasAccessibilityread non-observable system APIs)..id()forces SwiftUI to tear down and recreate the whole subtree — including the "Grant Screen Recording…"/"Grant Accessibility…"Buttons (SplashView.swift:405-421) — on every tick. If a click's mouseDown/mouseUp pair straddles one of those once-a-second identity resets, the button's action plausibly doesn't fire, since the view instance that captured the mouseDown is gone by the time the reset happens. This is the exact flow that unblocks the Pro picker, so an intermittently-swallowed click there is high-friction if it reproduces — but I can't confirm SwiftUI's precise behaviour under.id()churn without running it, so flagging as a question rather than asserting it.
⚡ Performance — 🟢 Approved
Nothing. previewLiveScrub/fontSize(for:) are cheap pure work run once per drag/scroll frame; the new ScrollValueAdapter local scroll monitors (one per draggable component field) and the ReadoutHeightKey/ColorPickers.swift:32-48 preference round-trip follow the same bounded, already-reviewed patterns as this file's earlier GeometryReader measurement passes.
📝 Docs — 🟢 Approved
The two stale cross-file references to the renamed clampValuesToRange (now finalizeValues) were caught and fixed within this same round (ea539b906, touching ColorDecomposition.swift:32,266 and WindowCoordinator.swift:350); grepped the whole tree for any I could still find — none remain.
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
The round-9 EyedropperButton.swift:12 VoiceOver/NSAccessibility ❓ is still open and unchanged; not repeated here per this review's own rules for non-refreshing findings. Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker/test suite were not run.
|
🐑 shepherd (automated) — round 18/21 review response This round's review (claude[bot] / github-actions[bot], "Approved with comments", #257 (review)) raised two ❓ questions, neither blocking:
No blocking findings. |
|
🐑 shepherd (automated) — round 18 review response / needs your decision Round 18 review (claude[bot] / github-actions[bot], "Approved with comments") raised two ❓ questions:
Options: (a) manually verify — fix if the undo step really does cross-contaminate; (b) leave as-is if you've already exercised this path without issue. To answer: 👍 this comment to go with my lean, or reply here with your call. |
The permission pills read non-observable system APIs (CustomColorPickSession .isAvailable / AXIsProcessTrusted), so a 1-second poll bumped permissionTick and an .id(permissionTick) on permissionArea forced the subtree to rebuild each tick to pick up a change. That tore down and recreated the Grant buttons once a second for the whole time the splash was open; a button press whose mouseDown/mouseUp straddled one of those identity resets could be swallowed — on the exact flow that unblocks the Pro picker. Mirror the two permission checks into @State instead, refreshed by the same poll and notifications. SwiftUI coalesces equal writes, so a re-render happens only when a permission actually flips, the pills keep a real SwiftUI dependency (fixing the original stale-pill bug the intended way), and the subtree keeps stable identity — removing the swallow hazard by construction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Approved with comments
Eighteenth round on this branch. Since the last control-room:review I posted (round 17, aedb0b9, "ready to merge"), 45 commits landed — the branch's other automated review workflow (review/review, the 🐑 shepherd bot replies visible on this PR's threads) ran across essentially all of them, catching and fixing a long list of real bugs in the new click-drag/scroll-to-scrub UX and its undo/redo wiring: the gamut-clamp commit-mismatch (scrub-end recomposed from a partly-stale values array instead of the already-correct live eyedropper.color, fixed in 0a768f3), frozenSize sizing to the current displayed width instead of the format's typed worst case (fixed in 627ffa5/9521d17), a dropped pill-clear on Escape-cancel (3d2c10a), an all-decimal hex value getting blanked on commit (2cc1ec4), and more. Two decisions were escalated and explicitly resolved by the author: focusedFieldUndoManager (AppDelegate.swift)'s reliance on AppKit's field editor being a single shared instance was verified with a standalone AppKit harness, and ClickShield's hit-test ordering (EyedropperButton.swift) was verified manually in a debug build. Scroll-to-scrub having no Escape-cancel path (unlike click-drag) was raised, discussed, and deliberately accepted as out of scope for this PR (recoverable via Cmd-Z since a scroll-scrub still commits to history).
I independently traced the current state of the core scrub/commit/undo logic — ScrubTextField.swift in full (new this round), EditableColorValue.swift's session lifecycle (startSession/previewLiveScrub/finishEditing/endSession), and AppDelegate.swift's triggerUndo/triggerRedo — end to end against the fixes above, and didn't find a case they miss. CI's Unit Tests pass on the current head (04a3fa0), including the new EditableColorValueTests.swift coverage for scrub reversibility and decimal-place derivation.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🟡 Comments |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is view-layout/interaction code and an undo-manager scoping fix, no input-handling or trust-boundary surface.
🧹 Code Quality — 🟡 Comments
- 💡
Pika/Views/EditableColorValue.swift:159— open, unresolved thread from the other review workflow:07fbaffgrouped every component with its trailing punctuation into one atomicFlowLayoutfragment so it can't wrap away from its value, butlayout.leading(e.g."oklch(") is still its own ungrouped fragment ahead of the loop — the same orphaning class07fbafffixed for trailing punctuation is structurally still possible for the leading affix if it and the first component don't both fit on line 1. The reviewer who raised it worked through thewrapShrinkTarget = 1.7shrink math and wasn't confident it's live today for this format set. I re-checked the same code this round and it's unchanged — still speculative, not confirmed reproducible, not blocking. - ✅ Everything else from that workflow's review threads on this round's files (
ScrubTextField.swift,EditableColorValue.swift,AppDelegate.swift,SplashView.swift,EyedropperButton.swift) has a "Fixed in ..." or "Verified" reply as of04a3fa0; not restating them individually here.
⚡ Performance — 🟢 Approved
Nothing new. previewLiveScrub's round-trip through decompose/recompose on every drag/scroll frame is the same shape of work the pre-existing live-preview path already did per keystroke; ColorPickers.swift's new ReadoutHeightKey preference read is deferred via DispatchQueue.main.async to avoid the same layout-pass reentrancy this codebase has already worked around elsewhere (EditableColorValue's width measurement).
📝 Docs — 🟢 Approved
The new ScrubTextField.swift and the rewritten scrub-lifecycle methods in EditableColorValue.swift carry the same standard of failure-mode-explaining comments this file has kept since round 14 (sessionStartValues, previewLiveScrub, rebudgetFrozenSize all explain the specific bug their design avoids, not just what the code does).
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run. For the AppKit-runtime-only questions (field-editor undo-manager sharing, ClickShield hit-test ordering) that can't be settled by reading alone, I relied on the manual/harness verification already recorded on this PR's threads rather than re-litigating them.
There was a problem hiding this comment.
🟡 Approved with comments
Eighteenth round on this branch. Since the last control-room:review I posted (round 17, aedb0b9, "ready to merge"), 45 commits landed — the branch's other automated review workflow (review/review, the 🐑 shepherd bot replies visible on this PR's threads) ran across essentially all of them, catching and fixing a long list of real bugs in the new click-drag/scroll-to-scrub UX and its undo/redo wiring: the gamut-clamp commit-mismatch (scrub-end recomposed from a partly-stale values array instead of the already-correct live eyedropper.color, fixed in 0a768f3), frozenSize sizing to the current displayed width instead of the format's typed worst case (fixed in 627ffa5/9521d17), a dropped pill-clear on Escape-cancel (3d2c10a), an all-decimal hex value getting blanked on commit (2cc1ec4), and more. Two decisions were escalated and explicitly resolved by the author: focusedFieldUndoManager (AppDelegate.swift)'s reliance on AppKit's field editor being a single shared instance was verified with a standalone AppKit harness, and ClickShield's hit-test ordering (EyedropperButton.swift) was verified manually in a debug build. Scroll-to-scrub having no Escape-cancel path (unlike click-drag) was raised, discussed, and deliberately accepted as out of scope for this PR (recoverable via Cmd-Z since a scroll-scrub still commits to history).
I independently traced the current state of the core scrub/commit/undo logic — ScrubTextField.swift in full (new this round), EditableColorValue.swift's session lifecycle (startSession/previewLiveScrub/finishEditing/endSession), and AppDelegate.swift's triggerUndo/triggerRedo — end to end against the fixes above, and didn't find a case they miss. CI's Unit Tests pass on the current head (04a3fa0), including the new EditableColorValueTests.swift coverage for scrub reversibility and decimal-place derivation.
| Category | Verdict |
|---|---|
| 🔒 Security | 🟢 Approved |
| 🧹 Code Quality | 🟡 Comments |
| ⚡ Performance | 🟢 Approved |
| 📝 Docs | 🟢 Approved |
| 🤖 Agents | 🟢 Approved |
🔒 Security — 🟢 Approved
Nothing. This round's diff is view-layout/interaction code and an undo-manager scoping fix, no input-handling or trust-boundary surface.
🧹 Code Quality — 🟡 Comments
- 💡
Pika/Views/EditableColorValue.swift:159— open, unresolved thread from the other review workflow:07fbaffgrouped every component with its trailing punctuation into one atomicFlowLayoutfragment so it can't wrap away from its value, butlayout.leading(e.g."oklch(") is still its own ungrouped fragment ahead of the loop — the same orphaning class07fbafffixed for trailing punctuation is structurally still possible for the leading affix if it and the first component don't both fit on line 1. The reviewer who raised it worked through thewrapShrinkTarget = 1.7shrink math and wasn't confident it's live today for this format set. I re-checked the same code this round and it's unchanged — still speculative, not confirmed reproducible, not blocking. - ✅ Everything else from that workflow's review threads on this round's files (
ScrubTextField.swift,EditableColorValue.swift,AppDelegate.swift,SplashView.swift,EyedropperButton.swift) has a "Fixed in ..." or "Verified" reply as of04a3fa0; not restating them individually here.
⚡ Performance — 🟢 Approved
Nothing new. previewLiveScrub's round-trip through decompose/recompose on every drag/scroll frame is the same shape of work the pre-existing live-preview path already did per keystroke; ColorPickers.swift's new ReadoutHeightKey preference read is deferred via DispatchQueue.main.async to avoid the same layout-pass reentrancy this codebase has already worked around elsewhere (EditableColorValue's width measurement).
📝 Docs — 🟢 Approved
The new ScrubTextField.swift and the rewritten scrub-lifecycle methods in EditableColorValue.swift carry the same standard of failure-mode-explaining comments this file has kept since round 14 (sessionStartValues, previewLiveScrub, rebudgetFrozenSize all explain the specific bug their design avoids, not just what the code does).
🤖 Agents — 🟢 Approved
Nothing. No AI/LLM surface in this round's change.
Reviewed by reading; dependencies aren't installed in this environment so the compiler/linter/typechecker were not run. For the AppKit-runtime-only questions (field-editor undo-manager sharing, ClickShield hit-test ordering) that can't be settled by reading alone, I relied on the manual/harness verification already recorded on this PR's threads rather than re-litigating them.
|
🐑 shepherd (automated) — status update What cleared: all 43 review threads resolved, the last pending decision ( |
Release integration branch for Pika 2.0.0.
Feature PRs merge into this branch;
2.0.0merges tomainwhen the release ships. Opens with theMARKETING_VERSIONbump to2.0.0.Landing in 2.0.0:
🤖 Generated with Claude Code