Plan 015: iced native notifications (3f), PageUp/PageDown scrollback, clipboard image paste, 3g docs - #304
Conversation
route_page(direction, viewport_rows) extends TerminalScroll with the same mode precedence as the wheel (mouse tracking -> Forward, alternate screen -> Forward with the real key encoded by the caller, else local). A local page moves exactly one full viewport, discards accumulated wheel fractions, and re-reads the authoritative scrollbar bottom state through the same path as the wheel: the shared tail is extracted into move_local_viewport so the Err conservatism lives in one place. Plan 015 C1. Review: codex at usage limit, cursor returned no output; careful self-review (sign convention, route() extraction parity, momentum reset, clamp semantics) found no issues. Gate: fmt, cargo test -p roost-vt --features ffi (24 passed), cargo test --workspace, workspace + roost-linux + roost-vt(ffi) clippy -D warnings all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
… policy Bare (zero-modifier) page keys on a live terminal route through roost_vt::route_page after keybind dispatch and before the pre-key snap: a LocalViewport verdict pages one full viewport with no PTY bytes and no snap, Forward (mouse tracking, alternate screen) falls through to the unchanged snap+encode path so today's \x1b[5~ family bytes keep flowing, modified combos never enter the branch. TerminalTab::handle_page refreshes its snapshot on the local route; selection state is untouched (fixtures pin selection survival and exact page-sized offsets, zero captured PTY bytes locally, exact forward bytes on alt-screen/tracking/shift). The Linux real-input check gains a physical PageUp/PageDown segment (local dump assertion + alt-screen forward capture) in the already-enumerated iced_clipboard_check.py. Plan 015 C2. Cursor review: no findings. Gate: make check-iced green (224 tests + clippy -D warnings + boundary greps), py_compile clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
…policy GTK twin of the iced adapter (77d776c): a bare page key (Page_Up/ Page_Down and keypad variants; Shift/Ctrl/Alt/Super/Meta disqualify, lock modifiers deliberately do not — GDK4 has no NumLock bit) routes through roost_vt::route_page before the pre-key snap and the #99 selection clear. LocalViewport consumes the key with a redraw and preserves the selection; Forward falls through byte-identical to today's snap/clear/encode path. handle_page_key is the testable seam; fixtures pin classification (incl. lock masking), full-page motion, and alt-screen/mouse-tracking forwarding at the policy level (a realized GTK widget is impractical headless — same coverage style as roost-vt's own suite). Plan 015 C3. Cursor review: no findings. Gate: fmt, cargo test -p roost-linux (147 tests, 0 failures), clippy -p roost-linux --all-targets -D warnings clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
DesktopNotifications drains an unbounded fire/retire channel on the engine runtime with one sequential worker: per-tab server ids give GTK's replace-not-stack semantics, each show is bounded by a 5s timeout that warns and keeps draining, and the per-OS backend seam ships Linux via notify-rust 4.18 (z-with-tokio: zbus 5 rides the existing tokio runtime, no async-io subtree) while every other target logs and returns None so a future UNUserNotificationCenter backend is a drop-in. Payload mirrors GTK: title verbatim, body omitted when empty, appname Roost, desktop-entry hint ai.stridelabs.Roost.iced. The NotificationFired arm now feeds the adapter beside the inbox upsert; TabClosed retires the slot. notify-rust is target-scoped to Linux (mac tree gains nothing) and the check-iced roost-engine grep now also forbids notify-rust/zbus/arboard. Plan 015 C4. Cursor adversarial review: retire-on-clear replace break FIXED (clear now keeps the server id — GTK's constant per-tab id is the parity bar); success-then-timeout id desync ACCEPTED as logged pathological-bus behavior (comment); wedged-daemon queue amplification ACCEPTED (same unbounded pattern as EngineFeed, plan risk note). Simplify pass: dead derives trimmed. Lock: +42 crates (21 compile on Linux, rest are notify-rust's non-Linux backends, never built). Gate: make check-iced (228 tests, extended grep), cargo test --workspace, workspace clippy -D warnings — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
The Linux backend adds the freedesktop default action; after each show the worker spawns one listener awaiting the click (opaque boxed future, so notify-rust types stay behind the cfg seam) and stores its AbortHandle in the tab slot — refire and retire abort the previous listener, so a tab never holds more than one, even against servers that emit neither ActionInvoked nor Closed. A click lands on the engine feed as NotificationActivated and drains in arrival order; the handler mirrors GTK reveal_and_focus_tab: core focus_tab (guards a closed tab), clear has_notification, reveal the sidebar, best-effort window raise via UiTask::Focus. Plan 015 C5. Cursor adversarial review: one finding — a click racing a same-tab replace clears the newer notification's pending flag — SKIPPED as parity-by-design: focusing a tab clears its pending state in every UI (sidebar clicks included), GTK's focus-tab action has the same no-generation semantics, and the user is on the tab when it happens. Gate: make check-iced (233 tests + boundary greps), fmt, Linux cross-check of the backend, no new dependencies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
An empty System-clipboard text read on the paste path now probes the
clipboard for image data (arboard 3.6, image-data +
wayland-data-control with X11 fallback) on the blocking pool, encodes
RGBA to PNG through the new shared png_encode helper (screenshot keeps
Compression::Fast; paste uses default), and pastes the temp-file path
through the tab's normal bracketed-paste wrap. Parity with the GTK
spec: 40*1024*1024 pixel cap pre-encode, 10 MiB cap on the encoded
output before any file exists, roost-image-{nanos}-{16hex}.png in the
OS temp dir with create_new + 0o600, no cleanup, no shell escaping
(the generated charset is the #282 defense-in-depth pin, unit-tested).
PRIMARY-selection paste never probes; missing tab or any failure is a
logged no-op; the clipboard queue resumes ahead of the probe
(start_next().then(probe)).
Known, plan-documented divergences: the pixel cap runs post-decode
(arboard decodes internally — weaker than GTK's size-prepared gate),
uri-list-only file copies stay GTK-only, and macOS is compiled but not
live-verified this pass.
Plan 015 C6. Cursor review: no findings. Simplify: png encoder
deduplicated into png_encode.rs. Lock: +18 crates, several
target-gated. Gate: make check-iced (240 tests), cargo test
--workspace, workspace clippy -D warnings — green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
…ated Roadmap: 3f marked complete (Linux D-Bus adapter, per-OS seam, replace semantics, click-to-focus; macOS + .desktop follow-ups in #303); 3g expanded into the four upstream-blocked gaps with the track-and- document disposition (#302); slice-order paragraph refreshed. Parity inventory: scrollback rows closed (wheel + page keys through the shared policy, Swift divergence recorded), drop rows re-scoped to the upstream list with the shipped clipboard-image adapter and its three documented divergences, notifications row implemented-on-Linux. CLAUDE.md library table gains notify-rust and arboard rows. Plan 015 C7. Docs-only; self-reviewed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe PR adds Linux desktop notifications, clipboard image materialization for Iced paste, and shared Page Up/Page Down terminal scrolling. It updates dependency boundaries, asynchronous task handling, tests, physical-input coverage, and Iced migration documentation. ChangesDesktop notifications
Clipboard image paste
Terminal page scrolling
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
crates/roost-iced/src/notifications.rs (1)
34-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove declaration comments that do not give non-obvious rationale.
crates/roost-iced/src/notifications.rs:34-100includes comments that restate private enum variants, fields, and type aliases. Keep the comments that explain ordering, replacement, and cancellation invariants.As per coding guidelines, “Default to no comments; add comments only for non-obvious rationale such as hidden constraints, workarounds, or tricky invariants.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/roost-iced/src/notifications.rs` around lines 34 - 100, Remove comments in Request, TabSlot, Activation, Shown, and Payload that merely restate their declarations or field meanings, while preserving comments documenting non-obvious replacement, ordering, cancellation, backend, or lifecycle invariants. Keep the implementation and derives unchanged.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/roost-iced/Cargo.toml`:
- Around line 38-45: Update the comment above the arboard dependency to remove
the inaccurate claim that arboard’s image dependency stays out of the picture,
while preserving the remaining explanations of image-data and
wayland-data-control.
In `@crates/roost-iced/src/app/interactions.rs`:
- Around line 1746-1753: Replace the direct TerminalTab::paste call in
deliver_paste_image with the shared workspace paste-image operation, passing the
target tab and image path. Update Iced and other frontend adapters to invoke
this operation and handle its result, preserving the closed-tab discard behavior
while ensuring terminal mutation and frontend behavior flow through the
workspace operation set.
In `@crates/roost-iced/src/main.rs`:
- Around line 479-499: Bound concurrent clipboard-image probes in the
PasteImageProbe handling of the update flow, coordinating with the queue-resume
behavior in interactions.rs so new requests cannot start unbounded
spawn_blocking tasks. Add or reuse a bounded concurrency mechanism or coalesce
pending probes, while preserving the existing PasteImageMaterialized success and
error message handling.
In `@crates/roost-iced/src/notifications.rs`:
- Around line 148-152: Update the Request::Retire handling to close the
server-side notification through the stored NotificationHandle before aborting
its listener. Use notify-rust 4.18.0’s close_async API, ensuring the close
operation is awaited or otherwise completed before slot.abort_listener() and
preserving the existing slot removal flow.
In `@crates/roost-iced/src/paste_image.rs`:
- Around line 99-110: Add deletion ownership for paths returned by
write_temp_png, retaining each materialized PNG for the paste lifetime and
removing it when its tab closes or the application exits. Thread cleanup
ownership alongside the PathBuf/String flow through main.rs and the terminal
interaction handling, ensuring both tab-close and shutdown paths remove all
temporary PNG files while preserving existing paste behavior.
In `@docs/development/iced-parity-inventory.md`:
- Around line 142-148: Update the later clipboard-image entry around the
adapter-work status to remove the stale deferral and reflect that clipboard
image materialization is shipped, matching the status already documented in the
File/image drops entry. Keep the inventory’s remaining adapter limitations and
documented divergences unchanged.
---
Nitpick comments:
In `@crates/roost-iced/src/notifications.rs`:
- Around line 34-100: Remove comments in Request, TabSlot, Activation, Shown,
and Payload that merely restate their declarations or field meanings, while
preserving comments documenting non-obvious replacement, ordering, cancellation,
backend, or lifecycle invariants. Keep the implementation and derives unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 54c49534-8daa-4f7d-ad35-d1132099711e
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
CLAUDE.mdMakefilecrates/roost-iced/Cargo.tomlcrates/roost-iced/src/app.rscrates/roost-iced/src/app/interactions.rscrates/roost-iced/src/app/servicing.rscrates/roost-iced/src/app/terminal_tab.rscrates/roost-iced/src/engine_feed.rscrates/roost-iced/src/input.rscrates/roost-iced/src/main.rscrates/roost-iced/src/notifications.rscrates/roost-iced/src/paste_image.rscrates/roost-iced/src/png_encode.rscrates/roost-iced/src/screenshot.rscrates/roost-linux/src/terminal_view.rscrates/roost-vt/src/lib.rscrates/roost-vt/src/scroll.rsdocs/development/iced-migration-roadmap.mddocs/development/iced-parity-inventory.mdtools/input/linux/iced_clipboard_check.py
| Request::Retire { tab_id } => { | ||
| if let Some(mut slot) = slots.remove(&tab_id) { | ||
| slot.abort_listener(); | ||
| } | ||
| continue; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline crates/roost-iced/src/notifications.rs --items all
rg -n -C 5 'Request::Retire|slots\.remove|abort_listener|NotificationHandle|close_async|wait_for_action_async' \
crates/roost-iced/src/notifications.rs
rg -n -A 4 -B 2 '^name = "notify-rust"$' Cargo.lockRepository: charliek/roost
Length of output: 4542
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- notification worker and backend ---'
sed -n '40,95p;140,238p;239,318p' crates/roost-iced/src/notifications.rs
printf '%s\n' '--- dependency declarations and all notify-rust references ---'
rg -n -C 3 'notify-rust|NotificationHandle|close_async|wait_for_action_async' \
Cargo.toml Cargo.lock crates
printf '%s\n' '--- retirement call site ---'
sed -n '445,475p' crates/roost-iced/src/app/servicing.rs
printf '%s\n' '--- notification tests ---'
sed -n '319,520p' crates/roost-iced/src/notifications.rsRepository: charliek/roost
Length of output: 20320
🌐 Web query:
notify-rust 4.18.0 NotificationHandle close_async wait_for_action_async API
💡 Result:
In notify-rust 4.18.0, the NotificationHandle struct provides async methods to interact with notifications, specifically on platforms utilizing the D-Bus protocol (XDG standards) [1][2]. These features are gated behind the zbus feature flag, which is enabled by default in many contexts [1][2][3]. NotificationHandle::wait_for_action_async This method returns a future that waits for the user to act on a notification and subsequently executes an invocation closure [1][2]. Method signature: pub async fn wait_for_action_async(&self, invocation_closure: F) where F: FnOnce(&NotificationResponse) NotificationHandle::close_async This is the asynchronous equivalent of the close method, used to programmatically close a notification [1][2]. Method signature: pub async fn close_async(&self) Important Considerations: Platform Limitation: These async methods are primarily available for the D-Bus (XDG) backend [1][2]. Specifically, close_async will panic if called with a D-Bus backend that does not support the operation [1][2]. Feature Gating: Ensure your Cargo.toml has the necessary features enabled (typically zbus) to access these async APIs, as they are not available on all platforms (e.g., they are generally not implemented for macOS or Windows) [1][2][4]. Usage: These methods are designed to be used in asynchronous Rust contexts, allowing you to await actions or closing events without blocking the thread [1]. Typical usage involves concurrently handling the notification's lifecycle, often by using a library like futures to zip or join multiple futures [1].
Citations:
- 1: https://docs.rs/notify-rust/latest/notify_rust/struct.NotificationHandle.html
- 2: https://docs.rs/notify-rust/latest/src/notify_rust/xdg/mod.rs.html
- 3: hoodie/notify-rust@v4.12.0...v4.18.0
- 4: https://docs.rs/notify-rust/latest/notify_rust/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import urllib.request
urls = [
"https://docs.rs/notify-rust/4.18.0/src/notify_rust/notification_handle.rs.html",
"https://docs.rs/notify-rust/4.11.3/src/notify_rust/notification_handle.rs.html",
]
for url in urls:
print(f"--- {url} ---")
try:
text = urllib.request.urlopen(url, timeout=10).read().decode()
for needle in ("struct NotificationHandle", "close_async", "wait_for_action_async", "impl Clone"):
pos = text.find(needle)
print(f"{needle}: {'present' if pos >= 0 else 'absent'}")
if pos >= 0:
print(text[max(0, pos-300):pos+900].replace(">", ">").replace("<", "<"))
except Exception as exc:
print(type(exc).__name__, exc)
PY
printf '%s\n' '--- manifest and lock resolution ---'
rg -n -C 4 'notify-rust' Cargo.toml Cargo.lockRepository: charliek/roost
Length of output: 1291
Close the displayed notification when the tab retires.
crates/roost-iced/src/notifications.rs:148-152 aborts the listener, but it does not close the server-side notification. Retain a close-capable NotificationHandle or send a close command to the listener before aborting it. Use the locked notify-rust 4.18.0 API, which provides close_async.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/roost-iced/src/notifications.rs` around lines 148 - 152, Update the
Request::Retire handling to close the server-side notification through the
stored NotificationHandle before aborting its listener. Use notify-rust 4.18.0’s
close_async API, ensuring the close operation is awaited or otherwise completed
before slot.abort_listener() and preserving the existing slot removal flow.
There was a problem hiding this comment.
Skipping as parity-by-design: neither shipped UI withdraws a delivered banner when its tab closes (GTK never calls withdraw_notification; Swift leaves delivered notifications in Notification Center), so closing on retire would exceed the parity bar and fork the shared semantics. A click on a stale banner is guarded — the activation handler drops activations for tabs that no longer exist (unit-pinned). If the product later wants withdraw-on-close it belongs to all three UIs at once; noted on #303.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- notification retirement ---'
sed -n '140,160p' crates/roost-iced/src/notifications.rs
printf '%s\n' '--- notification activation handling and tests ---'
rg -n -C 5 'NotificationActivated|focus_tab_and_clear|focus_tab_in_core|missing.*tab|closed.*tab|no.*tab' \
crates/roost-iced/src/app/servicing.rs crates/roost-iced/src/notifications.rsLength of output: 22566
@charliek, verification confirms the rationale.
crates/roost-iced/src/notifications.rs:148 removes the retired tab’s slot and aborts its activation listener. It intentionally does not close the delivered notification.
crates/roost-iced/src/app/servicing.rs:180-184 logs and ignores an activation when focus_tab_in_core cannot find the tab. crates/roost-iced/src/app/servicing.rs:465-467 retires the adapter state when a tab closes. The listener-retirement test is at crates/roost-iced/src/notifications.rs:528-533.
The requested close_async behavior would break the stated GTK and Swift parity. The finding remains withdrawn.
You are interacting with an AI system.
CodeRabbit PR #304: the arboard WHY comment claimed its image dep stays out of the picture — it decodes; only the PNG encode stays on the workspace png crate. The inventory's freestanding drops note still deferred clipboard-image materialization after the row above it recorded it shipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
Plan 015: iced slice 3f (native desktop notifications) plus the functional P1 remainders that 3e's visual polish won't cover. Panel-reviewed plan (GLM + CodeRabbit; Codex at its usage limit); seven gated commits; live-verified in the shed VM.
W1 — slice 3f: native desktop notifications (iced, Linux D-Bus)
crates/roost-iced/src/notifications.rs: aDesktopNotificationsadapter with a per-OS backend seam — Linux ships vianotify-rust 4.18(z-with-tokio: zbus 5 rides the app's existing tokio runtime, no async-io subtree; target-scoped to Linux so mac builds gain nothing); every other target is a log-only no-op, keeping a futureUNUserNotificationCenterbackend a drop-in (#303). One sequential worker on the engine runtime gives GTK's per-tab replace-not-stack semantics (stored server ids), each show bounded by a 5s timeout. Click-to-focus shipped: the freedesktop default action round-trips throughEngineFeed::NotificationActivatedinto GTK-parity reveal-and-focus (focus tab, clear badge, reveal sidebar, best-effort raise). Suppression stays engine-side (untouched, shared semantics); the adapter never runs inside engine locks — the inventory's pinned constraint, satisfied structurally. Listener lifecycle is bounded: abort-on-refire/retire keeps at most one live action listener per tab (panel finding).W2 — PageUp/PageDown local scrollback (both Rust UIs, shared policy)
roost-vt::TerminalScroll::route_pageextends the shared wheel policy with page-wise navigation under the identical mode precedence (mouse tracking → forward, alternate screen → forward with the real\x1b[5~/\x1b[6~bytes, else one full-viewport local page; wheel momentum discarded; authoritative bottom re-read incl. the Err conservatism, shared withroute()by extraction). Both UIs intercept bare page keys only, after keybind routing and before the pre-key snap — the snap bypass is conditioned on the policy's LocalViewport verdict, never the key; local pages preserve the selection; modified combos and forwarded bytes are byte-identical to before (fixture-pinned). GTK masks lock modifiers and accepts keypad variants. The Swift Mac app is deliberately unchanged (it has no such navigation; recorded in the inventory). The Linux real-input gate gains a physical PageUp/PageDown segment in the already-enumeratediced_clipboard_check.py— no new e2e module, no new IPC op.W3 — clipboard image materialization on paste (iced)
An empty System-clipboard text read on paste now probes for image data via
arboard 3.6(image-data+wayland-data-controlwith verified X11 fallback) on the blocking pool, encodes PNG through a new sharedpng_encodehelper (deduplicated with the screenshot module), and pastes the temp-file path through the normal bracketed-paste wrap. GTK-spec parity: 40·1024·1024 pixel cap, 10 MiB cap on the encoded output before any file exists,roost-image-{nanos}-{16hex}.png,create_new+0o600, no cleanup, no escaping (generated charset unit-pinned as the #282 defense-in-depth). PRIMARY paste never probes. Documented divergences (inventory row): pixel cap runs post-decode (arboard decodes internally — weaker than GTK's pre-decode gate), uri-list-only file copies remain GTK-only, macOS is compiled-but-unverified this pass.W4 — upstream-blocked gaps documented (3g discipline)
Roadmap 3f marked complete; 3g expanded into the four upstream iced/winit gaps (no drop coordinates, no text/URI-list drop events, no Wayland DnD, clipboard seat serial) with the pinned track-and-document disposition — no workarounds, no upstream engagement. New tracking issues: #302 (drop/DnD/Wayland gaps), #303 (notifications follow-ups). Parity-inventory rows updated to post-merge truth; CLAUDE.md library table gains notify-rust + arboard rows.
Verification
make check-iced(incl. an extended roost-engine boundary grep now also forbiddingnotify-rust|zbus|arboard),cargo test --workspaceon manifest-touching commits, full-D warningsclippy for the roost-linux commit. 240 roost-iced tests, 147 roost-linux tests, 24 roost-vt scroll fixtures at final state.iced_clipboard_check.pyreal-input gate PASS including the new page-key segment; notifications verified against a mockorg.freedesktop.Notificationsserver — payload/hints exact, live replace (replaces: <prev id>), and the full click-to-focus round trip (ActionInvoked on an inactive tab's banner switched focus and cleared only that tab's badge); image paste end-to-end (xclipimage/png→ pasted path → mode-600 PNG round-trips).Dependencies / risk
Two new deps, both iced-side only and boundary-grep-enforced: notify-rust (Linux-target-scoped; +42 lock entries, 21 compiled on Linux, 0 on mac) and arboard (+18 lock entries, several target-gated; brings a transitive second png codec — recorded wart, we encode via the existing
png0.18). Accepted risks dispositioned in review: wedged-daemon queue latency (unbounded-channel pattern shared with EngineFeed), success-then-timeout id desync (logged, pathological-bus only), banner-click-vs-replace race clears the tab's pending flag (parity-by-design — focusing a tab clears its badge in every UI).Plan 015 (panel-reviewed, full text)
See
~/.claude/plans/roost/015-iced-3f-and-functional-p1.md(local convention). Key pinned decisions: per-OS notification seam with no half-shipped macOS backend; bare-key page navigation with wheel-identical precedence (known cost: primary-screen apps lose forwarded bare\x1b[5~; apage_keysknob can be added backward-compatibly later); System-only image probe mirroring the GTK/Swift spec including its 0600 mode; docs-only treatment of upstream DnD gaps. Panel corrections adopted: listener abort-on-refire, snap-bypass-on-route-verdict wording, encoded-output cap (real GTK parity), selection preservation pin, GTK lock-modifier masking, engine-runtime pin for the worker, boundary-grep extension, objective fallback trigger for click-to-focus (not needed — it shipped).🤖 Generated with Claude Code
https://claude.ai/code/session_01WLKWsLV45DAk61xG6Utj3e
Summary by CodeRabbit