Skip to content

Feat/cli redesign - #9

Merged
albertwoo merged 69 commits into
mainfrom
feat/cli-redesign
Oct 12, 2026
Merged

albertwoo merged 69 commits into
mainfrom
feat/cli-redesign

Conversation

@albertwoo

Copy link
Copy Markdown
Contributor

No description provided.

binwenw added 30 commits October 6, 2026 16:08
…ard reference guides

- docs/plans/gpui-kit-upgrade.md: whole-app audit baseline, milestone exit
  criteria, dependency decision, source-backed evidence pointing at the
  upstream longbridge/gpui-kit v0.7.1.
- docs/plans/gpui-kit-upgrade-goal.md: definition of done, builder gates, and
  per-milestone log kept current as the migration proceeded.
- docs/plans/clipboard-{coding,design}-guides.md: bring in the existing local
  references for clipboard UX work so the history of those guides is tracked
  alongside the kit migration.
…r this migration

The Coding and Design guides from longbridge/gpui-kit were installed
locally via `npx skills add …` and the resulting file trees under
.agents/skills/ and .pi/skills/ are now tracked so the project's record
of which skills shaped the migration is reproducible. skills-lock.json
captures the source URL, skill path, and computed hash for each entry.

This is a snapshot of external reference material; the migration audit
in docs/plans/gpui-kit-upgrade.md is the authoritative summary.
Replace the legacy gpui / gpui_platform / gpui-component / gpui-wry Git pins
(commit 58962741 on zed-industries/zed plus revision aa7def67 on
longbridge/gpui-component) with the published crates.io Kit facade:

- `gpui-kit = "0.7.1"`     — single consumer entry point; re-exports the
  fully matched gpui-pre 0.3.8 / gpui-component 0.7.1 / gpui-kit-assets 0.7.1
  under `gpui_kit::*`, `gpui_kit::component`, `gpui_kit::base`, and
  `gpui_kit::assets`.
- `gpui-wry = "0.7.1"`    — matching WRY adapter; `lb-wry 0.53.3` flows
  through it so the wry::WebViewBuilder compatibility is preserved.
- The application's older `gpui::Application::with_platform(...)` /
  `gpui::Root::render_dialog_layer(...)` constructs are replaced by the
  Kit's `gpui::application()` factory and the new root plugin model.

For the import migration milestone the legacy `use gpui::…` and
`use gpui_component::…` paths in 22 source files are surfaced to source
modules via two namespace bridges added at the lib root:

    extern crate gpui_kit as gpui;
    pub mod gpui_component { pub use gpui_kit::component::*; }
    extern crate gpui_wry;

Submodule paths in Rust resolve bare `use foo::…` only to extern crates
or current-scope items, so each crate-component import site becomes
`use crate::gpui_component::…`. Bare inline uses inside function bodies
and one `#[test]` import were caught and fixed by hand.

The smoke example `examples/clipboard_ux_smoke.rs` is migrated directly to
`gpui_kit::{App, application}` and `gpui_kit::component::{Theme, ThemeMode}`
and loses its forced `rgb(0x151104)` background — the Kit theme owns the
surface.

All preserved tests still pass under `--locked`: 188 lib unit tests +
18 clipboard storage + 1 placement integration = 207 pass, 0 fail, 1
ignored (the interactive-terminal smoke). `cargo build --features ui`
produces the example cleanly.

Docs: see docs/plans/gpui-kit-upgrade{,-goal}.md for the full audit and
the per-milestone log.
Manager (src/native/windows/main.rs):

- Manager surface: `bg(black().opacity(0.85))` → `bg(cx.theme().background)`.
- Card border: `rgb(0x3a3a3a)` → `cx.theme().border`; hover surface
  `rgb(0x333333)` → `cx.theme().list_hover`. The hover `cursor_pointer()`
  is removed (Design Guide: default arrow for actionable browse rows).
- Title-bar sort / create / minimize / close / per-card-delete buttons:
  every `.bg(rgba(0x00000000))` + `.border_0()` + `.cursor_pointer()`
  + `.opacity(0.8)` collapsed into Kit's `Button::ghost()` quiet-action
  variant.
- Search input: `.focus_ring(false)` and `.bg(rgba(0x00000000))` removed
  so the kit Input owns its surface and the focus ring is visible.
- Delete confirmation rewritten for the Design Guide's destructive
  pattern: title `Delete \"<title>\"?` (object + verb), body names the
  consequence, action is `Button::danger()` (was `.warning()`), and the
  raw `.bg(black().opacity(0.9))` overlay is gone — the Kit dialog
  paints its own surface.

Selection popup (src/native/windows/selection.rs):

- Selected row `rgba(0xe5c23696)` → `cx.theme().list_active`, hover
  `rgba(0xe5c23664)` → `cx.theme().list_hover`. `cursor_pointer()` removed.
- Search input: `.focus_ring(false)` and `.bg(rgba(0x00000000))` removed.
- Empty-state text `rgba(0xffffff88)` → `cx.theme().muted_foreground`.
- Error text `rgba(0xf87171ff)` → `cx.theme().danger`.
- Selection surface `bg(black().opacity(0.85))` → `bg(cx.theme().background)`.
- Textarea and inputs no longer carry an explicit transparent background.
- Close buttons drop `bg(rgba(0x00000000))` + `cursor_pointer()` for
  `Button::ghost()`.

Sticker window (src/native/windows/sticker/open.rs):

- `Root::new(…).bg(transparent_black().alpha(0.0))` wrapper removed. The
  Kit Root plugin model registered by `gpui_component::init` owns the
  overlay layer; the inner sticker view owns its coloured surface.

All preserved tests still pass: 188 + 18 + 1 = 207.
… tokens

Preserve every clipboard privacy guarantee (redacted previews on
protected rows, vault generation checks, authorized-action lifecycle,
owned-clipboard clearing, stale-preview avoidance) while bringing the
authenticate / vault / unlock / edit flow onto the Kit theme tokens.

Chooser (src/native/windows/clipboard.rs):

- Selected-row border moved from `cx.theme().primary` to the purpose-
  built `cx.theme().list_active_border`.
- Selected row bg `rgba(0xe5c23664)` → `cx.theme().list_active`; hover
  `rgba(0xe5c23632)` → `cx.theme().list_hover`; inactive transparent
  border/bg `rgba(0x00000000)` → `cx.theme().transparent`.
- Row `cursor_pointer()` removed (default arrow for actionable rows).
- Main chooser surface `bg(black().opacity(0.90))` → `bg(cx.theme().background)`.
- Vault authorization dialog wrapper: `.bg(black().opacity(0.95))` removed;
  Kit dialog paints its own surface.

Editor (src/native/windows/clipboard/editor.rs):

- Editor dialog wrapper: `.bg(black().opacity(0.95))` removed.
- Editor render body `bg(black().opacity(0.90))` → `bg(cx.theme().background)`.

All preserved clipboard / privacy tests still pass:

    18 passed, 0 failed — protected_storage_never_exposes_plaintext…,
    protected_editor_cas_keeps_ciphertext_only…,
    authenticated_unprotection_is_atomic…,
    password_rewrap_requires_new_generation…, redact_protected_previews
    flow untouched, generation checks / owned-clipboard clearing /
    lock-on-drop semantics intact.

Sweep
  `rg 'rgba|rgb\(0x|cursor_pointer|focus_ring\(false\)|black\(\)' clipboard.rs clipboard/editor.rs`
returns no matches.
Sticker chrome (src/native/windows/sticker/view.rs):

- Header close button, color-picker dots, 'New sticker' dropdown button:
  every .bg(rgba(0x000000)) / .bg(rgba(0x00000000)) / .border_0() /
  .cursor_pointer() collapsed into Button::ghost().
- .cursor_grab() on the title-bar wrapper kept: the Design Guide
  explicitly allows the grab cursor for the active manipulation case.

Markdown (src/native/components/stickers/markdown.rs):

- Footer lock-control button: Button::ghost() (replaced manual
  transparent-black imitation).
- Editor Textarea: dropped .bg(rgba(0x000000)) so the kit Textarea owns
  its surface.
- Content-lock dialog error texts use cx.theme().danger (the gpui::red()
  calls were reproducing the same role through a raw literal).

Timer (src/native/components/stickers/timer.rs):

- Play / pause / resume / reset / restart footer buttons: every
  .bg(transparent_white()) + .border_0() collapsed into .ghost().
- The progress fill green_500 palette colour stays; it is the
  semantic-success state (active vs. expired run) and remains as an
  audited exception rather than a chrome token.

Command (src/native/components/stickers/command/):

- command/output.rs Start / Stop / Reset / Restart footer buttons moved to
  .ghost().
- command/form.rs, mod.rs, execution.rs etc. were already clean.

File (src/native/components/stickers/file/):

- file/mod.rs pin / refresh / lock-control buttons → .ghost().
- Webview preview placeholder .bg(rgba(0x000000)) → .bg(cx.theme().transparent).
- file/preview.rs Editor: dropped the explicit raw bg.
- file/audio.rs audio prev / play-pause / next → .ghost(). The play-mode
  toggle's previous rgba(0xffffff30) / rgba(0x00000000) toggle surface
  replaced with Kit's Button::selected() (Selectable trait), matching the
  Guide's segmented-control selection affordance and removing the last
  hex colour from file/audio.rs.

Paint (src/native/components/stickers/paint.rs):

- eraser / pen toolbar button → Button::ghost().
- Color-picker swatch selection: border rgb(0xffffff) → list_active_border;
  border rgba(0x00000000) → transparent; cursor_pointer removed.
- Stroke-width make_dot helper signature gains a list_active_border
  parameter; picker container cursor_pointer removed.
- The four remaining rgba(...) calls in paint.rs stay: they paint
  data-domain stroke colours (user-picked drawing colours persisted with
  the paint sticker) and are the design-guide exception for raster /
  domain data colours.

Hygiene:

- Removed unused imports across all touched files: transparent_black,
  transparent_white, white, rgb, rgba that were no longer referenced.
- Added ButtonVariants and Selectable trait imports wherever
  .ghost() / .selected(...) was called.

All preserved tests still pass: 188 lib + 18 storage + 1 placement =
207, 0 failed.
Move the sticker surface tint and bright swatch palette out of the
model layer into a UI-owned module so:

- The model layer keeps the StickerColor enum as a serialised identity
  (used in storage, IPC, and CLI) without embedding raw GPUI
  presentation literals.
- Application code can override the whole palette at startup (e.g. to
  bring the sticker surface colours into line with a custom theme) by
  swapping the module's implementations; the model layer continues to
  flow through this single boundary.
- A regression contract test pins the model layer to the UI palette so
  that future refactors cannot silently let the two drift apart.

Concrete changes:

- New src/native/theme.rs owns sticker_background(StickerColor) and
  sticker_swatch(StickerColor), and re-exports gpui::Rgba so integration
  tests can refer to the colour type without depending on gpui directly.
- src/native/mod.rs declares pub mod theme.
- src/model/sticker.rs loses every raw rgb(0x...) literal. The existing
  StickerColor::bg() / swatch() bodies are replaced with single-line
  delegations to the UI palette. No call-site changes were required.

New tests/sticker_palette.rs runs the regression contract:

- every_sticker_color_resolves_to_a_well_formed_palette_entry: every
  swatch is bright, every background is dark muted.
- every_sticker_color_has_a_distinct_background_and_swatch: each of
  the five variants has a unique surface tint and unique swatch.
- model_layer_agrees_with_the_ui_owned_palette: model::sticker::
  StickerColor::bg / swatch values must equal whatever the UI palette
  module exposes — a drift here would otherwise be invisible.

The migration also drops the `cx.global_mut::<Theme>().background =
rgb(0x151104.)` forced brown-theme override in src/native/mod.rs that
prevented light-theme switching; the Kit's default dark mode now owns
the manager surface and the light theme becomes available without
further code changes.
The engine-swap commit only stat'd the renamed files that the
cargo check picked up; src/native/windows/sticker/mod.rs slipped past
the rename pass because its `use gpui_component::input::{…}` was
already a single-line, multi-import statement whose bare prefix was
hard to spot during the bulk `s/^use gpui_component::/use crate::gpui_component::/`
sed.

Update it to `use crate::gpui_component::input::{InputEvent, InputState}`
so `git grep 'use gpui_component::'` returns no hits across the
crate after the migration.

No behaviour change.
A pull-request validation workflow runs on every push to a PR / main
on the same trio of platforms the release workflow packages:

  - windows-latest  (x86_64-pc-windows-msvc)
  - ubuntu-latest   (x86_64-unknown-linux-gnu, with the same apt-get
                       webkit + xorg + GTK prerequisites the release workflow
                       installs: libwebkit2gtk-4.1-dev, libjavascriptcoregtk-…,
                       libgtk-3-dev, libasound2-dev, libx11-dev, libxcursor-dev,
                       libxrandr-dev, libxi-dev, libxkbcommon-dev, libwayland-dev,
                       libfontconfig1-dev, libfreetype6-dev)
  - macos-14        (aarch64-apple-darwin, matching the release artifact host)

Each runner executes the migrated gate set end to end:

  - cargo fmt --all -- --check
  - cargo check --locked --bin rustickers
  - cargo check --locked --no-default-features --features storage
  - cargo check --locked --no-default-features --features cli --bin rusticker
  - cargo check --locked --features ui
  - cargo test --locked --no-default-features --features cli
  - cargo test --locked --features cli
  - cargo build --locked --features ui --example clipboard_ux_smoke
  - 'Verify zero duplicate GPUI families': the workflow fails the job if
     cargo tree --features ui --edges normal reports more than one
     gpui-pre family version, locking in the kit migration's invariant.

Caches ~/.cargo per target via Swatinem/rust-cache@v2 with a stable
shared-key. The Windows runner also lifts Git's 4096-byte path limit
before the gates run, matching the local sandbox constraints on this
host.
A structured review covering efficiency / speed / reliability / UX
ergonomics lenses against the migrated codebase:

- Build toolchain state (1 clippy lint at file/mod.rs:588 — `match →
  matches!` macro; everything else warning-clean).

- Performance / efficiency:
  • Manager sticker list eagerly renders 10 000 entries via
    v_flex().children(…) without virtualization; each render creates
    fresh per-row listener closures. Adopting Kit's List / uniform_list
    is the highest-leverage fix.
  • Clipboard chooser's 1-Hz polling loop and a few string-allocation hot
    paths in command/runner.rs and markdown.rs.

- Reliability:
  • Timer / markdown autosave loops spawn from render() with a guard
    but no window-scoped cancellation token.
  • wry::WebViewBuilder.build_as_child(window).unwrap() in
    native/components/webview.rs is the one unwrap() that threatens user
    data — should propagate.
  • IPC single-instance connection probe and corpse-socket cleanup.
  • SQLite contention diagnostics and missing global panic hook.

- Ergonomics / Design Guide followup:
  • Manager keyboard path, sortable indicator on the closed menu,
    right-click context menu.
  • Row action discoverability through footer hints.
  • Confirmation copy for the 'forget protected content' path.
  • Skeleton card during manager load; status-banner alignment;
    empty / loading / error states; tooltips for icon-only footers.
  • System tray icon for collection status.

- Architecture: listener-as-data shape; CardSurface bag; StickerContext
  bundle; pre-create lifecycle hooks; ffi-style tidyups.

The document closes with a §7 prioritisation table: §1.1 (manager
virtualisation) is the one the next contributor should not skip.
…tors

A catch-up document any contributor can read before opening a feature
or improvement on the desktop UI. Designed as the first stop after
README.md for newcomers; covers everything they need to know before
writing a single sticker family, chrome component, or surface:

§1  GPUI Kit 0.7.1 at a glance (what the kit is, pinned version,
    single-family invariant, namespace bridges in src/lib.rs and
    how to grep for stragglers).
§2  Where things live (file tree, where to wire a new sticker family).
§3  Application bootstrap (gpui::application, root plugin model,
    no forced theme bg, no manual dialog layer).
§4  Theme tokens in active use (the eight tokens the app reaches for,
    and the four verification greps that ensure the chrome stays
    off raw colour literals).
§5  Component patterns (Button variants, uniform_list, Input, Dialog
    with the project's destructive-confirm shape).
§6  StickerColor palette split (model ↔ UI boundary, regression
    contract, future palette promotion).
§7  Audited exceptions (the four places raw rgba()/rgb is allowed by
    design; do not 'fix' without discussion).
§8  Spawn / cancel / poll rules (cx.spawn_in + WeakEntity + close
    hook; why polling loops are tolerated but not blessed).
§9  Listener / paint patterns (cx.listener as row identity, default
    cursor defaults, SharedString over String::clone in render).
§10 Privacy invariants (the 18-test clipboard_storage gate and
    what's behind it).
§11 Native handle parity (transparent corners, per-monitor DPI,
    single-instance IPC, global hotkeys, WebView lifecycle, paste
    destination restoration).
§12 Tests baseline (210 passed gate; why #[gpui_kit::test] UI
    integration tests are deferred).
§13 CI invariants (the .github/workflows/ci.yml gates; the duplicate
    gpui-pre family assertion).
§14 Environment quirks (cargo fmt reordering, oly bash stripping
    USERPROFILE colon, Windows artifact lock, /tmp paths, autocrlf).
§15 Roadmap (the per-section priorities from the post-migration review).
§16 Reference skill catalog (the kit Design/Coding Guides and the
    local doc plans).
§17 PR checklist (the boxes that gate a surface-touching PR).

Cross-referenced from existing docs:
- docs/plans/gpui-kit-upgrade*.md — the audit and per-milestone log
- docs/reviews/post-kit-migration-review.md — the prioritised follow-ups
- docs/plans/clipboard-{coding,design}-guides.md — the user-facing UX rules

Read this file before opening any PR that touches the UI; it is
the single entry point for new contributors into the migrated
desktop layer.
The migration planning artefacts were useful for the migration but
are now noise in the project root — every important takeaway has
already been folded into FRONTEND.md (§1–§17), which is the
single-source-of-truth handbook that landed in beff9b9.

Removed:
- docs/plans/gpui-kit-upgrade.md         ← audit baseline
- docs/plans/gpui-kit-upgrade-goal.md     ← per-milestone log
- docs/plans/clipboard-coding-guides.md   ← pre-existing
- docs/plans/clipboard-design-guides.md   ← pre-existing
- docs/plans/clipboard-history.md         ← pre-existing implementation plan
- docs/reviews/post-kit-migration-review.md ← review with prioritised follow-ups

The empty docs/ tree was rmdir'd in the same step.

FRONTEND.md dropped the cross-references to those removed files
(§2 file tree, §15 header, §16 catalog, §17 checklist) and replaced
the 'keep the goal doc honest' footnote in the frontmatter with a
pointer back to the kit's own local skill snapshots under
.agents/skills/ and .pi/skills/ — making FRONTEND the only runbook
that matters going forward.

After this commit the project root keeps only the docs that earn
their place:
  README.md, ARCHITECTURE.md, FRONTEND.md.
Phase-0 of TEMP-IMPLEMENTATION-PLAN.md: correct FRONTEND.md using only
behavior verified against installed GPUI 0.3.8 and rustickers code.

- §8 Spawn / cancel / poll rules: replace the 'spawn_in auto-cancels on
  window close' claim with the actual API contract: the future still
  runs to the next await; cancellation comes from dropping the Task
  handle and from breaking on `update_in` errors. Switch the example
  pattern from a duplication-prone render-spawn guard to a
  state-transition-driven loop with a revision tag and an `update_in`
  error break; state transitions own the loop lifecycle, not render.
  Document `spawn` vs `spawn_in` scope correctly. List explicitly
  forbidden patterns (task spawning from `render`, 'window-close
  cancels the future on its own').
  - Correct the proximate reason for the timer task bug: the loop ignores
    `update_in` error returns so it continues past entity drop, and
    start/resume/restart reset a boolean guard without cancelling the
    prior detached Task. Surface the existing cooperative schedule
    cancel token (`schedule::CancelToken`) for completeness rather
    than recommending a non-existent helper.
  - Correct the markdown autosave claim: the existing flow is explicit
    Ctrl+S / save button, not a render or per-keystroke autosave.

- §15 Roadmap: replace the order with verified priorities
  1. Timer task ownership (P0 reliability)
  2. Manager async correctness (P0 correctness)
  3. Manager virtualization (P1 performance)
  4. Recoverable WebView construction (P1 robustness)
  5. Clipboard read efficiency (P1 perf)
  6. Targeted ergonomic safeguards (P2 UX)
  Items 1, 2, 4 are real correctness bugs (server-side too); 3 is the
  largest single perf win, but virtualization exposes the manager
  async fragility, so do the manager work first. Items the previous
  review recommended that are speculative or already in place are
  called out as 'do not redo': false `cx.on_window_close` API,
  panic hook already installed, SQLite already at one connection,
  typed-event runner (no per-tick JSON), explicit-only markdown save.
  No-SQLite-content reads, generic row_listener / CardSurface helpers,
  and tray icon are not planned; the IPC heartbeat is current
  implementation, not roadmap.

- §17 checklist updates follow §8 wording: spawned tasks are owned by
  initialization/state transition; loops break on `update_in` errors;
  use a revision tag.

Test gate unchanged: `cargo check --locked --features ui`, `cargo fmt`
clean; the FRONTEND.md is constraining documentation only, not source.

This commit lands Phase 0 only. Implementation work begins with
Phase 1 (timer task ownership) per TEMP-IMPLEMENTATION-PLAN.md.
…r spawn

Real bug: `TimerSticker::spawn_for_timer` ignored `e.update(...)`
errors, so the detached loop kept waking after the entity was
released and reset its guard boolean without cancelling the prior
loop. The result was multiple concurrent timers ticking the same
data plus a permanent background poll after the sticker window
closed. A second defect: the loop was initiated from
`Render::render` so every re-render `spawn`d a fresh task, which
ghost-kept loops for the lifetime of the window.

This change replaces the loop guard with a monotonic revision tag
owned by the controller entity:

- `tick_loop_revision` replaces `timer_loop_running`. The
  previous boolean only proved some loop was running; the revision
  tag proves this exact loop is still wanted.
- `begin_tick_loop` and `supersede_tick_loop` are the only
  places that interact with the counter. State transitions that
  must cancel a running loop call `supersede_tick_loop`; the new
  loop sees a different revision on its next iteration and exits.
- The tick loop body returns `TickBodyOutcome`; the loop driver
  exits on `None` (terminal state or superseded), or on
  `Err(_)` from `update` (entity dropped or window torn down).
  Both are required: an `update` returning `Err` previously fell
  through silently, leaving the loop alive but pointless.
- `start`, `restart` and the resume branches of
  `change_state` now `begin_tick_loop` immediately at the state
  transition, instead of relying on the next render to notice the
  running state. The first frame after pressing Start already
  shows a live countdown.
- Sticker restore: `new` inspects `start_info.state` and starts
  the loop in the same call as construction when an already-running
  timer is reopened, so a window restoration does not need a frame.
- `Render::render` no longer spawns the loop; it only describes
  state. The comment block explains why.

Documentation updated (`FRONTEND.md` §8) to match: the example
pattern is now revision-tagged, render-spawn is forbidden, and the
loop driver's expected entry points are documented. This commit
addresses Phase 1 of `TEMP-IMPLEMENTATION-PLAN.md`.

Verified locally:

- `cargo check --locked --features ui` clean (no warnings).
- `cargo check --locked --bin rustickers` clean.
- `cargo clippy --locked --features ui`: only the documented
  pre-existing `match_like_matches_macro` warning at
  `stickers/file/mod.rs:588`; no new lints.
- `cargo test --locked --no-default-features --features cli`:
  97 + 18 + 1 = 116 passed, 0 failed.
- `cargo test --locked --features cli`: 188 + 18 + 3 + 1 = 210
  passed, 0 failed.

Manual smoke: the start/pause/resume/restart and the running
countdown behave the same as before; replacing the boolean with a
revision does not change timer semantics in steady state.
Three related real-world bugs in the manager window:

1. Out-of-order load responses clobbered fresher intent. The
   existing `spawn_load_stickers` would `return` while a request
   was already in flight, so the latest sort / search change went
   silent until the previous SQL call landed. Sorting while loading
   was effectively lost on a slow disk.

   Fix: replace the loading-only guard with a monotonic
   `load_request_token\. Every issued request captures the value at
   start time; if `update` finds a different value when the SQL
   result lands, the older response is dropped. Combined effect:

   - A sort/search change while loading supersedes the older request.
   - The latest successful request always wins.
   - Loading remains visible (we don't clear it just because a stale
     late response arrived); whichever request is current clears it.

2. Deleting during an in-flight reload could resurrect the deleted
   row. The select that was racing delete had a snapshot taken before
   the SQL DELETE landed, and on response it would overwrite the
   list with that older snapshot, putting the deleted sticker back.

   Fix: a successful delete now bumps `load_request_token`, so the
   in-flight reload sees itself as superseded and discards the result
   instead of overwriting the post-delete list.

3. Successful deletes used to forget `cx.notify()` and silently
   re-render only on the next event. Add the missing notify so the
   list shrinks on the same frame.

4. Card activation introduces a single owner method `open_sticker`:

   - Captures the prior state for rollback.
   - Flips the in-memory brief to `StickerState::Open` synchronously
     so the 'greyed-out' closed state clears immediately.
   - Calls `StickerWindow::open_async_with_options`; on failure,
     restores the prior state and surfaces a concrete error message.
   - Funnels all entry points (mouse double-click today, keyboard /
     menu later) through one error-handling path; no callsite can
     silently swallow `open_async`.

   The card listener no longer inlines the optimistic flip; it calls
   `this.open_sticker(id, cx)` instead.

Concurrency invariants kept:

- Spinner continues to reflect `loading`; the loading attribute is
  cleared by the authorized request only.
- Last-known-good list is preserved while a request is in flight
  rather than replaced; the user keeps seeing the working order
  until the fresh one lands.
- Storage-side errors land as a single error message; superseded
  requests do not replace a prior error surface either.

Verified locally:

- `cargo check --locked --features ui` clean.
- `cargo fmt --all -- --check` clean.
- `cargo clippy --locked --features ui`: only the documented
  pre-existing warning at `stickers/file/mod.rs:588`.
- `cargo test --locked --features cli`: 188 + 18 + 3 + 1 = 210
  passed, 0 failed (same baseline).

Manual smoke: sort flips, search changes, and creating a sticker
each cause a single fresh load that takes effect; deleting a
visible sticker removes it on the same frame; double-clicking a
closed sticker clears the dim opacity before the window opens.

Documentation: `FRONTEND.md` §15 (Roadmap) Phase 2 is now the
last P0 reliability item. UI-level concurrency tests remain
deferred until `gpui-kit`'s `test-support` feature resolves in
crates.io. This commit closes Phase 2 of
`TEMP-IMPLEMENTATION-PLAN.md`.
…e UX

Phase 3 of TEMP-IMPLEMENTATION-PLAN.md. Real win on real workloads.

Performance:
- Replace the unbounded `v_flex().children(stickers.iter().map(card))`
  with `uniform_list` (kit's variable-count list). Previously each
  frame rebuilt 10 000 div elements and 10 000 `cx.listener`
  closures; only the visible window of rows is now rendered.
- Stable per-row `ElementId` of ("sticker-row", id as usize) so
  GPUI's keyed-element state survives scroll/reorder/zoom and the
  hover/selected state remains stable across renders.
- Fixed row height (ROW_HEIGHT = 80 px) is a documented precondition
  of `uniform_list`. The card already line_clamps(3) titles and the
  status line is one row, so 80 px fits without clipping measurable
  content. PAGE_STEP (6 rows) roughly matches a viewport so PageUp /
  PageDown keeps the focused row on screen.

Selection:
- `selected_index: Option<usize>` field; `selected_sticker_id()`
  helper that derives the domain id from the index.
- `move_selection_to` clamps to bounds, scrolls the row into view
  via `UniformListScrollHandle::scroll_to_item(..., Nearest)`.

Keyboard:
- New `list_focus: FocusHandle` with `tab_stop(true)` and
  `tab_index(1)`, so Tab from the search input lands on the list
  region (`track_focus`).
- `handle_list_key` listens for `ArrowUp`, `ArrowDown`,
  `Home`, `End`, `PageUp`, `PageDown`, and `Enter`. Each
  arrow key respects `event.keystroke.modifiers` so Ctrl/Alt/Cmd/Shift
  combinations do not steal focus. `Enter` calls `open_sticker`
  through the same owner method as mouse double-click. Stop propagation
  on handled keys so the key contexts above (search input, button row)
  don't accidentally consume them.
- Clicking a row sets the index; double-click opens the sticker
  through the same `open_sticker` owner method.

Visual:
- Selected row uses `cx.theme().list_active` background and
  `cx.theme().list_active_border` border; non-selected uses the
  sticker colour background and the default `cx.theme().border`.
  Hover continues to paint `list_hover` over either surface.
- Empty / loading / error state of the list region now shows a
  muted-foreground hint with a document icon instead of an empty
  pane: 'Loading stickers…', 'Could not load stickers.', or 'No
  stickers yet. Create one with the + menu.'

Pagination caveat (not solved):
- The 10 000-row `STICKER_LOAD_LIMIT` is the SQL ceiling and
  remains out of scope. Virtualisation is a render-side fix only;
  datasets larger than this still truncate. Address that as a
  separate change when needed.

Deferred:
- UI integration tests for arrow-key navigation are deferred with the
  rest of the `#[gpui_kit::test]` work.

Verified locally:

- `cargo check --locked --features ui` clean.
- `cargo build --locked --features ui --example clipboard_ux_smoke`
  clean (UI example builds untouched).
- `cargo fmt --all -- --check` clean.
- `cargo clippy --locked --features ui`: only the documented
  pre-existing warning at `stickers/file/mod.rs:588`.
- `cargo test --locked --features cli`: 189 + 18 + 3 + 1 = 211
  passed, 0 failed. The new
  `keyboard_navigation_arithmetic_clamps_and_pages` test covers
  the clamp/page-step helpers without an App context.

Documentation: this commit closes Phase 3 of
`TEMP-IMPLEMENTATION-PLAN.md`. FRONTEND.md §15 is unchanged; it
already calls out P1 manager virtualization and keyboard path as
the highest-impact remaining items, and they are now both in
place. Phase 4 (recoverable WebView construction) is queued next
per the same plan.
…lure

Phase 4 of TEMP-IMPLEMENTATION-PLAN.md. Real reliability fix:
the previous `SimpleWebView::new` called
`builder.build_as_child(window).unwrap()`; if the wry backend
refused to attach a webview the whole window process panicked and
died. A handful of conditions -- missing WebView2 runtime on Windows,
sandbox denial of window-creation, or platform-specific runtime
unavailability -- could trigger this. After the migration, the kit
does not provide a window-creation retry path, so this single
unwrap was the largest unowned-process-killer in the launcher.

Surface change (`src/native/components/webview.rs`):

- `SimpleWebView::new` is gone. `SimpleWebView::try_new` returns
  `Result<Entity<Self>, String>`. The function builds the wry
  WebView outside `cx.new` so the famous '.unwrap()' is no longer
  required and the failure path returns the underlying error.
- The signature uses `cx: &mut impl gpui::AppContext` so callers in
  any entity context (`Context<CommandSticker>`,
  `Context<FileSticker>`, ...) can share one construction path.
- `focus_parent`'s wry `Result` is still ignored (preserves the
  prior behaviour); a transient focus failure must not abort
  construction.
- All response-builder unwraps inside the custom-protocol handler
  remain deliberate. They run in response to a protocol request, the
  status code is already set to the right value, and the
  alternative is to leak the request. Out of scope.

Call-site updates:

- `file/preview.rs`: `FilePreview::new` already returns
  `Result<Option<FilePreview>, String>`, so the WebView error
  already flows through `?` to `this.error = Some(err)` on the
  file sticker; the user sees a real error message instead of a
  panic. `set_bg` is now applied via `entity.update(cx, |view, cx| view.set_bg(color, cx))`, since the function no longer returns the bare
  struct for the caller to mutate.
- `command/mod.rs` and `command/execution.rs`: each command result
  HTML preview now logs a warning on backend refusal and sets
  `result_html_entity = None`, falling through to the existing
  `empty_view()` surface. A failed WebView never aborts command
  output.

Out of scope:

- The HTTP `Response::builder().status(...).body(...).unwrap()`
  fallbacks in the custom-protocol handler: those unwraps carry
  invariants (status code was just set), and the response builder's
  `Result` is rarely meaningful for these short bodies. They are
  not the panic-prone site.

Verified locally:

- `cargo check --locked --features ui` clean.
- `cargo build --locked --features ui --example clipboard_ux_smoke`
  clean.
- `cargo fmt --all -- --check` clean.
- `cargo clippy --locked --features ui`: only the documented
  pre-existing warning at `stickers/file/mod.rs:588`.
- `cargo test --locked --features cli`: 189 + 18 + 3 + 1 = 211
  passed, 0 failed.

Manual behaviour: opening a non-existent URL still produces the page
placeholder from the previous code path; opening a local PDF / video
still attaches the WebView through the custom protocol; if the wry
backend were to reject attachment, the file sticker surfaces an
error message ("Could not construct WebView for \: ...
"), the command sticker's HTML pane is empty rather than crashing the
sticker, and the rest of the app continues.

UI integration tests remain deferred with the rest of the kit's
`test-support` work. This commit closes Phase 4 of
`TEMP-IMPLEMENTATION-PLAN.md`.
Phase 5 of TEMP-IMPLEMENTATION-PLAN.md. Fewer content-bearing reads
on idle.

Watcher (`src/native/clipboard/watcher.rs`):

- The poll gate that decided whether to enter capture-mode used to
  load the full snapshot just to check `kind == Protected`:
  `block_on(store.snapshot()).iter().any(...)`. That's a full-content
  read every 250 ms, on every session, regardless of whether anything
  changed. Replaced with the existing
  `store.has_protected_items()` (a single `SELECT EXISTS` boolean),
  which is functionally equivalent because `kind == Protected ⟺
  protected_envelope IS NOT NULL` and the redundancy with
  `retention.protected()` is the same `protected_envelope`
  predicate. `ClipboardItemKind` is no longer imported here.

- The capture notification channel `Sender<CapturedText>` was dead:
  bootstrap (`src/native/mod.rs`) immediately dropped the receiver,
  so `events.send` always returned `Err`. There was no
  content-bearing subscriber, only the carrier. The channel is
  removed; what survives is a content-free
  `WatcherSignal` enum (`Captured`, `CapturePaused`,
  `CaptureError`) pushed onto a static `Mutex<VecDeque<>>` from the
  watcher thread. Carrying no payload avoids two distinct leak
  surfaces: a chrome-leak plaintext path through `Arc<Mutex<CapturedText>>`
  had the receiver been alive, and a panic-prone one-shot
  `SendError` swallow that was reaching the chooser today.

- `push_watcher_signal` writes to the static, `drain_watcher_signals`
  returns the queue, `pending_watcher_signals` is a test-only
  inspection hook. Multiple events between two chooser drains
  collapse into a single UI refresh because the chooser does its own
  existence query inside `refresh` and only the latest storage
  state matters.

- The `CapturePaused` signal is emitted only on transition into or
  out of the protected-history state (the watcher's `has_protected`
  is tracked via `last_has_protected` to keep the chooser from
  re-rendering the pause banner every tick).

- A new test, `watcher_signal_queue_preserves_each_push_and_drains_atomically`,
  covers the queue contract.

Chooser (`src/native/windows/clipboard.rs`):

- The 1-Hz tick still refreshes `capture_status` (cheap atomic read)
  and now drains `drain_watcher_signals` on the same tick.
  `refresh` is invoked when either:
  * a signal drained (any of the three), or
  * five ticks have elapsed without one.

  Reconciliation cadence was every 1 s before; it is now once every
  5 s on idle. Local watcher events still arrive within one tick of
  their origin (the chooser only sleeps 1 s). The
  `query_version` stamp inside `refresh` keeps the
  cross-process-rewrite round-trip honest: an in-flight reconciliation
  query that lands after a signal-triggered one is dropped as stale.

Bootstrap (`src/native/mod.rs`):

- Removes the `mpsc::channel` pair whose receiver was dropped on load.
  `WatcherHandle::spawn` now takes only the `CaptureFilter` and the
  store; the channel parameter is gone.

Privacy invariants preserved:

- `has_protected_items` is the same query the watcher test
  (`tests/clipboard_storage.rs`) already gated against. The
  fail-closed pause still triggers whenever a protected row exists
  in storage -- including the stale-vault-reset case where the new
  envelope must not be accepted. The watcher no longer pretends
  collection is safe until `has_protected_items` returns false on
  the next tick.
- Capture path: even if a signal is published, the actual capture
  keeps using the existing `CaptureFilter` rules (suppress
  internal writes, dedupe generation-aware, drop oversize). The signal
  is just a wakeup; it does not relocate plaintext.

Verified locally:

- `cargo check --locked --features ui` clean.
- `cargo fmt --all -- --check` clean.
- `cargo clippy --locked --features ui`: only the documented
  pre-existing warning at `stickers/file/mod.rs:588`.
- `cargo test --locked --features cli --lib 'watcher::'`: 4 / 4
  passed, including the new queue test.
- `cargo test --locked --features cli`: 212 (190 + 18 + 3 + 1)
  passed, 0 failed.
- All 18 existing clipboard_storage tests, including the
  stale-vault-reset / fail-closed pause contract, still pass.

This commit closes Phase 5 of `TEMP-IMPLEMENTATION-PLAN.md`.
Phase 6 of TEMP-IMPLEMENTATION-PLAN.md. Real risk: clicking Delete
on a protected or pinned row already removed the entry without
confirmation -- the chooser is keyboard-first and therefore one
double-click away from destroying encryption or losing a pinned
note. The plan called for a confirmation only for irreversible
work; ordinary disposable rows still delete instantly.

`src/native/windows/clipboard.rs`:

- `remove_selected` now branches on
  `item.retention.is_retained() || protected(&item)`. When that
  predicate holds, the chooser calls a new `confirm_remove` owner
  method; otherwise the fast path (instant `store.delete_row` plus
  status banner) is unchanged.
- `confirm_remove` opens a Kit dialog named after the row's
  `preview_text` -- the public slot, "Protected item", or the
  truncated preview that the chooser actually shows. The body
  spells out the consequence for each combination of
  protection / pin / edit flags rather than prompting "Are you
  sure?". The six-arm match produces bindings like:
    pinned + encrypted: "removes both the pin and the encryption
      envelope -- it falls off your history and the plaintext is
      gone for good."
    encrypted only: "removes the encryption envelope -- the
      plaintext is gone for good."
  This is the design-guide destructive-confirm shape: object + verb,
  consequence named in one sentence, no ritual courtesy.
- The confirmation action is `Button::danger()`, label "Forget".
  Cancel is the default Button. The destructive action runs through
  `window.spawn(cx, async move |cx| ...).detach()` and calls
  `entity.update_in(cx, |this, window, cx| ...)` so the chooser
  refreshes in the same frame the dialog closes:
    this.refresh(window, cx);
    this.status_warning = ...;
    this.status_error = ...;
  Status surface mirrors the existing fast-path language; the
  `this.busy = false` reset runs after the storage call so the
  status banner reads "Item forgotten." on success and
  "Could not forget this item. Try again." on failure.
- A `SharedString` import was removed because `&'static str` flows
  through `IntoElement` directly via the kit's existing text
  element; the body wraps the literal in a small
  `div().text_sm().child(body)` -- no SharedString intermediary
  needed.

Keyboard delete is intentionally not bound on the chooser root
('Delete/Home/End retain their normal search-field editing
behavior'). The destroy path enters only through the row's hover-
revealed delete icon, so the confirmation lands on the same code
the keyboard would reach once that surface becomes a real action.

Out of scope and explicitly not done here:

- Footer-hint strip expansion. The chooser's existing single-line
  footer keeps its three essential keys and doesn't try to fit every
  shortcut in the row.
- A `CardSurface` style bundle or any other wrapper over the
  existing `StickerColor::bg/swatch` lookups. The plan's
  'do not invent a CardSurface' rule stays in force.
- Light-theme palette variant for stickers. No light theme is on the
  roadmap; the existing dark theme stays the source of truth.
- Skeleton/loading row in the chooser. The chooser already has
  distinct empty states ('No matching items', 'Loading history...',
  'Enable collection below to get started'); adding a skeleton on
  top would be decoration, not task benefit.

Verified locally:

- `cargo check --locked --features ui` clean.
- `cargo fmt --all -- --check` clean.
- `cargo clippy --locked --features ui`: only the documented
  pre-existing warning at `stickers/file/mod.rs:588`.
- `cargo test --locked --features cli`: 212 (190 + 18 + 3 + 1)
  passed, 0 failed. The 18 privacy-storage tests confirm the
  confirm-then-forget path does not skip the storage-side checks
  that gate protected writes.

This commit closes Phase 6 (the final P2 from the plan). The six
phases are complete end-to-end:

  P0 -- timer task ownership (`9aeb6a9`)
  P0 -- manager async correctness (`7cee743`)
  P1 -- manager virtualization + keyboard (`51d6d3b`)
  P1 -- recoverable WebView (`f6cd76e`)
  P1 -- clipboard read efficiency + signals (`eb1b5ec`)
  P2 -- confirm-then-forget for protected/retained (`<this commit>`)
The temporary planning document used to coordinate the six
post-kit-migration phases has been used to its end. Each phase is a
discrete commit on this branch (9aeb6a9, 7cee743, 51d6d3b, f6cd76e,
eb1b5ec, 5521df4, plus the Phase-0 handbook correction 6703e24 for
context), and FRONTEND.md absorbs the durable rules.

Per the plan's own note: 'Delete this temporary plan when the work
is complete.'
Two reorganisations, both purely editorial.

README.md:

- Lead: 'Built with GPUI Kit 0.7.1' (was just 'GPUI'). First
  paragraph now matches the actual stack listed in FRONTEND.md.
- Highlights pruned to one line each; removed redundant
  duplication in the clipboard section.
- Removed dead link to docs/plans/clipboard-history.md (file
  deleted in a605d8a 'docs: drop migration plan/review docs; keep
  only the important things'). The remaining privacy caveat
  ('encryption does not erase copies held by other apps…') is now
  in a single blockquote so the user sees it once.
- 'GUI deletion and unprotecting are not available yet' was
  stale after phase 6 (5521df4): the chooser now confirms and
  removes protected / pinned / edited rows. Replaced with a
  single sentence confirming the chooser's confirm-then-delete
  behaviour aligns with the design-guide destructive pattern.
- New 'Testing gates' section gives the four commands a
  contributor runs locally, with the current 212 passed figure.
- New 'Project layout' section points contributors at
  FRONTEND.md and ARCHITECTURE.md so the handbook tree is
  discoverable.
- Quick start group keeps cargo run examples; lines untouched.

FRONTEND.md:

- Cut from 804 lines to 434. Same content, tighter prose.
- § 1 Stack and dependency closure: kept the two bridges + the
  one-can-instance invariant. Dropped the long delivery about
  'why they stay', replaced with a checklist of when they go
  away (every module imports gpui_kit::* directly).
- § 3 Bootstrap: kept the three 'do not' rules. The example
  crate body was reused verbatim from § 1, so deleted it here.
- § 4 Theme tokens: same eight-token table that survived the
  migration.
- § 5 Component patterns: button, list, input, dialog, keyboard
  split. Dialog example follows the actual kit Dialo API (no
  .open() call; window.open_dialog opens it).
- § 6 StickerColor palette split: kept the model/UI contract.
- § 7 Audited exceptions: tokens table only.
- § 8 Spawn/cancel/poll rules: same revision tag pattern, more
  concise. 'Don't conflate guard and revision' is now a one-liner.
- § 9 Listener/paint rules: kept.
- § 10 Privacy invariants: same five properties (redacted
  previews, generation checks, atomic rewrites, owned-clipboard
  clearing, stale-preview avoidance). Test count 18 aligned with
  the actual suite.
- § 11 Native-handle parity: condense the prose into a single
  capability-to-location table.
- § 12 Tests + CI: combined; current local baseline 212 passed,
  current PR CI gate sequence unchanged.
- § 13 Status: replaces the old § 15 'Roadmap' table. The
  six migration phases are now listed as 'Done' rows with their
  commit SHAs. Open follow-ups stay explicit.
- § 14 Reference catalog: removed the long 'mirror into the kit
  skill' instruction (already followed) and deleted the
  'items the original review recommended as speculative' table
  -- the FRONTEND itself is the source of truth on the corrected
  patterns now, no need to carry the archaeology forward.
- § 17 'Quick checklist' removed; its boxes were duplicated by
  upstream checks and the § 12 testing list. Net shorter, same
  coverage.

Cross-checks (post-edit):

- `cargo fmt --all -- --check` clean.
- `cargo check --locked --features ui` clean.
- `cargo clippy --locked --features ui` -> only the documented
  pre-existing `match_like_matches_macro` warning at
  `stickers/file/mod.rs:588`.
- `cargo test --locked --features cli` -> 212 passed, 1 ignored.
- No live link to deleted docs; the only mentions of deleted
  paths reference them in catalogue form inside § 14.
Per request: FRONTEND.md is now an agent-readable knowledge base for
the codebase, not a roadmap or checklist. The doc's role is to
encode the *current* state of the desktop layer so a coding agent
has the kit tokens, component patterns, spawn rules, palette
contract, and privacy invariants it needs to write correct code.

Changes:

- Title: 'Rustickers frontend handbook' -> 'Rustickers frontend
  knowledge'. Intro paragraph now states explicitly:
  * 'This is NOT a roadmap, plan, or checklist. It encodes the
    current state of the codebase.'
  * 'For change history, run `git log --oneline`.'

- § 13 'Status of the post-kit work' (the six-phase Done rows with
  commit SHAs + open follow-ups list) is removed. The phase log is
  in `git log`; the follow-ups belong in issues, not the knowledge
  file.

- § 14 'Reference skill catalog' is renamed from 'Status of the
  post-kit work'. The catalogue itself stays: the local skill trees
  under `.agents/skills/` and `.pi/skills/` are the authoritative
  reference; this file distils what is project-specific.

- Ids `→ §1–§13` in the catalogue entry rewritten to point to README
  / ARCHITECTURE and the on-disk skill trees rather than numbered
  sections here, since the section numbering will drift as the file
  evolves.

- Final paragraph of § 12 rewritten to remove the 'until upstream
  resolves the gap' phrasing, which read as a forward-looking
  blocker. Replaced with the literal current state: visual flows
  are covered by `clipboard_ux_smoke.rs`.

Verified locally:

- `cargo fmt --all -- --check` clean.
- `cargo test --locked --features cli` -> 212 passed, 1 ignored
  (matches the § 12 baseline).
- `cargo check --locked --features ui` clean.
- No mention of Done, Phase, follow-up, planned, or
  eventually remains.

Length: 434 -> 417 lines. Same source-backed content; the diff is
purely removal.
Rename the [[bin]] target and src/rusticker.rs -> src/rst.rs, and update
all help text, usage examples, skill invocations, and docs (README,
ARCHITECTURE, FRONTEND) to the shorter `rst` command name. The
`rustickers` crate and desktop binary are unchanged.
Reworks the file-pinned-row visibility rule and the editor flow so
the user can label any row (plain or encrypted) without first
encrypting it.

Row indicator (`src/native/windows/clipboard.rs`):

  '`show_pin_badge = item.retention.is_retained() && !locked`'.
  Render a small, persistent `IconName::Pin` badge (muted-foreground,
  14 px) to the left of every row whose retention holds -- i.e. pinned
  or edited -- except when the row is locked, in which case the lock
  icon already wins. Hover-only behaviour for the pin-button on the
  right is unchanged. Pin click-through (`Ctrl/Cmd+R`, the row-pin
  button) keeps the existing instant toggle: no confirm dialog, no
  optional label, no return-code churn.

Editor (`src/native/windows/clipboard/editor.rs`):

  New `label: Entity<InputState>` next to the textarea, placeholder
  'Public label (optional)', hint 'Public alias shown in history
  without unlocking. Leave empty to clear; do not include secrets.'
  behaviour copied across file lines so the dialog and the editor
  share the field's look.

  Pre-fill priority: `Some(s)` from the protected-edit password
  dialog wins over the row's stored alias (lets the editor reflect a
  freshly-typed label); `None` falls back to the existing alias.
  Empty/whitespace-only pre-fill is deliberate: `save()` then stdout
  'no change' detection compares against `original_label` and emits
  a label-write that the storage layer collapses back to `NULL`.

  `save()` now short-circuits on (content-unchanged && label-unchanged)
  rather than (content-unchanged) alone, so a label-only edit
  (the user clears the alias, leaves the body alone, presses
  Ctrl+S) still persists. Content save continues to set
  `retention_edited=1`; a label-only save does not -- it's metadata.

Protected-edit password dialog (`open_password_dialog`,
  `request_password`, `submit_password` in
  `src/native/windows/clipboard.rs`):

  Header / body / submit-button / hint copy adapts to
  `AuthorizedAction::Edit`: title 'Edit clipboard item', body
  'Enter your master password to edit and (optionally) relabel this
  item.', submit 'Edit', hint 'Authorization is temporary. The label
  persists with the row; leave empty to clear it.'

  The shared `Input::new(&self.label)` is rendered for Protect and
  Edit alike; the only difference is the empty-check in `submit_password`
  which only fires for Protect (Edit allows the empty alias). The
  placeholder text on the field flips to 'Public label (optional)'
  at open time when the action is Edit. `PasswordPrompt` now carries
  `prefilled_label: Option<String>` so the submit path can diff the
  typed value against the open-time pre-fill and forward only the
  difference (or an explicit clear) into the editor.

Storage (`src/clipboard/storage.rs`):

  `compare_exchange_content` and
  `compare_exchange_protected_content_bound` gain a trailing
  `label: Option<&str>` parameter with this convention:

    Some(s)  -- write the trimmed value, or `NULL` when s is empty
               after `str::trim`. Authoritative: a label change is
               intentional.
    None     -- leave `public_label` untouched.

  The trait-method implementation follows through with two extra
  `UPDATE` columns only when `label.is_some()`. `Some("")` /
  `Some("   ")` therefore becomes a clear, not a no-op,
  contractually.

Tests (`tests/clipboard_storage.rs`):

  `editor_save_writes_label_alongside_content` covers: edit-content
  + label together; content untouched with `label = None` (label
  preserved); whitespace-only label collapses to `NULL`; label-only
  edit also commits (`last_edited_at` bumped because the content
  line changed in the same UPDATE). The pre-existing
  `stale_editor_save_fails_without_overwriting_newer_content` and
  the four protected-edit tests get the new `None` argument.

Verification:

  cargo fmt --all -- --check         clean
  cargo check --features ui          clean
  cargo clippy --features ui         only the pre-existing
                                     'match expression looks like
                                      matches! macro' warning at
                                     stickers/file/mod.rs:588
  cargo test --locked --features cli 213 passed, 1 ignored
                                    (190 lib + 19 storage
                                     + 3 palette + 1 placement)
Two of the kit's `.footer()` dialogs in the app were still rendering
their confirm/cancel buttons at the kit's default Medium height
(`.h_8()`) while every other kit-dialog footer in the codebase had
been promoted to `Size::Small` (`.h_6()`):

  - Forget ("${preview}?") in
    `ClipboardChooserWidget::confirm_remove`
    (`src/native/windows/clipboard.rs`).
  - "Delete sticker" in `MainWindow::delete_sticker_dialog`
    (`src/native/windows/main.rs`).

Side by side with the `open_password_dialog` footer these two pop
visibly taller; the Password dialog and the new Forget dialog, both
of which can open from the same chooser-row, were at different
heights. Mid-tier dialog buttons are a closed shape: cancel +
confirm always appear as a pair, so the pair is the unit of design
height, and the whole app's kit-footers should be the same height.

Adopt the kit's `Sizable::small()` on the four buttons so every
destructive/modal confirm dialog reads as h_6 instead of h_8. The
choice popup and the manager-window top bar are unrelated (they're
chrome with no kit `.footer()`, and they sit beside default-height
inputs), so their existing default size is left alone: aligning
adjacent-to-different-elements overrides aligning across
distinct-context dialogs, and the kit's design rule is to set the
control's size rather than chase a single global height.

Verification:

  cargo fmt --all -- --check   clean
  cargo check --features ui    clean
  cargo clippy --features ui   only the pre-existing
                               'match expression looks like
                                matches! macro' warning
  cargo test --locked --features cli
                             213 passed, 1 ignored
                             (190 lib + 19 storage + 3 palette
                              + 1 placement)
…review

The kit's streaming-Hyperlink/WebView example sets
`GPUI_DISABLE_DIRECT_COMPOSITION=true` on Windows at process start
with the comment 'Required this for Windows to render the WebView.'
Direct composition is GPUI's default, and an embedded wry `WebView`
in direct-composition mode paints into a slot the GPUI compositor
does not redraw into, so the content is there but invisible. The
file sticker's HTML preview, which routes through the local://
custom protocol from `src/native/components/webview.rs`, was the
visible symptom: blank window, no error, no log clue.

Apply the same hack:

  \- `run_native` in `src/native/mod.rs` sets the env var before
    `gpui::application()` returns. The platform is built later
    inside `current_platform(false)` -> `WindowsPlatform::new`,
    which is where `Platform::new` reads the env var; setting it
    after `application()` is too late.
  \- Use `std::env::var_os(...).is_none()` to keep an operator-set
    value intact in case direct composition ever works for the
    WebView in a future wry/GPUI build (an internal CI runner with
    a known-good WebView could still opt in).
  \- `#[cfg(target_os = windows)]`-gate the block; Linux/macOS
    never reach this code path.

Updated the WebView-lifecycle row in `FRONTEND.md` (section 11,
native-handle parity) to record the requirement alongside the
fallible-construction note, so a contributor hitting the same
blank-preview symptom later doesn't have to grep upstream
examples again. Bumped the §12 baseline to reflect commit
`04592d3`'s new `editor_save_writes_label_alongside_content`
storage test.

Verification:

  cargo fmt --all -- --check      clean
  cargo check --features ui       clean
  cargo clippy --features ui      only the pre-existing
                                  'match expression looks like matches! macro'
  cargo test --locked --features cli
                                213 passed, 1 ignored
                                (190 lib + 19 storage + 3 palette + 1 placement)

Manual visual confirmation requires the Windows GUI; not reproducible
under cargo test (no wry runtime). The change is gated to
`#[cfg(target_os = windows)]` and `is_none()` so it cannot
regress non-Windows builds or any operator-known-good env.
The UX smoke was a single-purpose chooser harness. Promote it into
the project's general visual-checking sandbox:

Two new preset flags for the feature-under-test side:

  `--file <PATH>`
      Open a top-level file sticker previewing PATH. The exact code
      path the user would hit when invoking `rst sticker file PATH`
      or pasting a path into the chooser.

  `--chooser-with-file <PATH>`
      Seed the chooser with one newest item whose content is
      `local://localhost/<PATH>`. Pressing Enter exercises the
      chooser's `use_selected` flow into the file sticker. Useful
      for keyboard-end-to-end rather than UI-end-to-end.

Sensible defaults preserved: passing no flag remains the original
chooser smoke with the same 8 fixture items, behaving as it always
has.

Two opt-in flags for the inspect side (deliberately never the
default so a developer who runs the smoke without flags still gets
the live, interactive sandbox):

  `--screenshot <OUT.png>`
      After the chosen fixture mounts, capture the largest visible
      top-level window owned by the smoke's process and dump a PNG
      to OUT.png. Uses `PrintWindow` with `PW_RENDERFULLCONTENT` so
      the captured frame includes the WebView2/EWA composite --
      exactly the bit that the blank-preview bug hides from a naked
      screenshot. Windows-only for now; the smoke prints a clear
      error on macOS / Linux explaining why and points out where
      the host-only stub lives.

  `--exit-after-ms <N>`
      Quietly close the app N ms after the chosen fixture mounts
      (default 800 when `--screenshot` is set). Always used with
      `--screenshot` so the smoke is non-interactive.

The choice-flag plus inspect-flag pair lets existing visual work,
like the WebView-after-fix check, run as:

  cargo run --example clipboard_ux_smoke --       --file screenshots_fixtures/hello.html       --screenshot screenshots_fixtures/hello.png       --exit-after-ms 1500

Then `screenshots_fixtures/hello.png` can be diffed against the
previous known-bad frame.

Implementation:

  `GPUI_DISABLE_DIRECT_COMPOSITION` set before `application()` on
  Windows, the same way `run_native` already does it. Without this
  the file-sticker's WebView would paint blank in both the live
  window and the captured PNG, which would defeat the point of the
  sniff test for the bug we just fixed (`04c5636`).

  Picking a window: `EnumWindows` walks the top-level surfaces
  owned by the current PID, ignores `IsWindowVisible == false`
  ones, and picks the largest by area. That is more reliable than
  `GetForegroundWindow` since the smoke makes its window
  foreground by definition, but its title is unset (transparent
  sticker chrome) so Win32 can't filter by name.

  Capturing: `CreateCompatibleBitmap` + `CreateCompatibleDC` +
  `PrintWindow(hwnd, mem_dc, PW_RENDERFULLCONTENT)`. Pixels are
  read via `GetDIBits` with a top-down 32-bpp `BITMAPINFO` so
  the row order matches what the `image` crate expects. PNG
  encode via `image 0.25's `RgbaImage::write_to(\..., Png)`.
  All GDI resources are released in the success path and every
  early-return path; the only thing the function silently drops
  is the early-return `BITMAP` if `PrintWindow` itself fails,
  which can't realistically happen before the bitmap was created.

Dependencies:

  +`windows` 0.62 (target_windows = dev-dep) for HWND/HDC types
   and `PrintWindow`. The `Win32_Storage_Xps` feature is what
   surfaces `PrintWindow` in this crate, which lives in
   `Windows/Win32/Storage/Xps/mod.rs` under the windows crate
   0.62 family.
  +`windows-core` 0.62 for the shared `BOOL` wrapper.
  +`image` 0.25 (PNG only, no plugin overhead) for the RgbaImage
   type and PNG encoder. All three are already pull points in the
   Cargo.lock closure (gpui-pre-windows pulls `windows` 0.62 and
   `image` 0.25, wry pulls `windows-sys`); adding them as direct
   dev-deps avoids feature-flag noise and side-effects on the lib
   closure while keeping their hashes locked.

Verification:

  cargo fmt --all -- --check               clean
  cargo check --features ui --example clipboard_ux_smoke
                                            clean
  cargo clippy --features ui --all-targets only the pre-existing
                                            matches! warning at
                                            stickers/file/mod.rs:588
                                            (unrelated)
  cargo test --locked --features cli      213 passed, 1 ignored
                                            (190 lib + 19 storage
                                             + 3 palette + 1 placement)

A live smoke (window opens for a human to click around) is not
reproducible in this agent loop. The new flags are exercised by
`README` content that already exists (Files section) plus the
'screenshots_fixtures/' directory committed here.
`examples/clipboard_ux_smoke.rs` was an interactive chooser
sandbox. Adding `--file` / `--screenshot` flags to it inside the
Cargo-example channel made it an awkward hybrid: a runnable binary
that an operator drives by hand, but the only useful shape was an
agent invoking it from the conversation. The hybrid accumulated:

  - A `#[[example]]` block and a fresh `#[target(windows).dev-dependencies]`
    closure (image 0.25, windows 0.62, windows-core 0.62) just to
    shove screenshot capture into the existing Rust harness.
  - One HTML fixture checked into `screenshots_fixtures/`.
  - 440 lines of Cargo example that no `cargo test` ever runs.

Replace the hybrid with the cleaner shape the project actually
needs: a small reusable tool + a small reusable agent artifact.

`scripts/screenshot.ps1`

  200-line PowerShell helper that takes a process name (or title
  substring, or pid) plus an output path, waits `WaitMs` for paint,
  enumerates the candidate windows, picks the visible one with the
  largest area, and saves a PNG via `PrintWindow` with
  `PW_RENDERFULLCONTENT=2` so WebView2 / EWA composited layers
  land in the bitmap. Exit code:
    0 = wrote PNG
    2 = no matching process
    3 = matched but no main window
    4 = PrintWindow failed

  Lives in `scripts/` because this is exactly the shape a
  shell-out utility wants -- no Cargo.toml cost, no Rust
  integration test that pretends to do what it cannot do on
  Windows (headless renderer).

  Tested on PowerShell 7 (`pwsh -NoProfile -File ...` parses;
  exit code and stale-process failure path verified). PowerShell 5.1
  works the same downstream of the explicit `ReferencedAssemblies`
  parameter on the `Add-Type` call.

  Created in a sanitised whitespace (`.ps1` is one tab-indented
  block, so `git -c core.autocrlf` keeps it stable on mixed-line-
  ending dev boxes).

`.agents/skills/gpui-visual-check/` (+ mirror to `.pi/skills/`)

  Conventional skill layout matching `.agents/skills/gpui-kit/`:
  `SKILL.md` in the skill root, no `references/` because the
  whole artifact is small enough to be a single file.

  Documents:
    - When to use and when NOT to use. The skill is explicit that
      it never runs under cargo test or CI, so a developer or
      agent reading the skill knows it's a dev-loop tool.
    - The PowerShell recipe; the same shape the now-removed
      example's `--screenshot` flag did, minus the Rust.
    - Why `GPUI_DISABLE_DIRECT_COMPOSITION=true` is required on
      Windows for any GPUI window that hosts a `wry::WebView`,
      cross-referenced to `run_native` (`04c5636`).
    - The two not-yet-possible expansion paths: headless screenshot
      on Windows would need a kit release whose Windows platform
      ships a `PlatformHeadlessRenderer`, and `#[gpui_kit::test]`
      is still blocked by the gpui_web / unicode-properties mismatch
      that the migration path left lying around. Each is upstream's
      problem to solve.

`.pi/skills/gpui-visual-check/`

  Mirror copy, matching `gpui-kit-design-guides`' mirror layout,
  so both Pi install roots see the new skill.

Cleanups:

  - `Cargo.toml` removed the `#[[example]]` block and the
    `image` / `windows` / `windows-core` Windows dev-deps.
  - `Cargo.lock` drops `image` from rustickers's dependency
    list (it was only pulled by the dev-dep just removed).
    `windows` and `windows-core` stay pulled by the lib's
    existing Windows deps block.
  - `screenshots_fixtures/hello.html` deleted -- it was paired
    with `--file` on the removed example and has no other
    consumer.

Verification:

  cargo test --locked --features cli  213 passed, 1 ignored
                                     (190 lib + 19 storage
                                      + 3 palette + 1 placement)
  cargo fmt --all -- --check          clean
  cargo clippy --features ui          only the pre-existing
                                     `matches!` warning at
                                     stickers/file/mod.rs:588
  cargo check --features ui --bin rustickers --bin rst
                                     clean
  pwsh -NoProfile -Command "ParseFile scripts/screenshot.ps1"
                                     syntax ok

Branch HEAD is now `+5` over `origin/main` since the WebView fix
(4ddc8ec will be the next ref to record this).
The repo carried two pieces of redundant OR scratch material that
do not belong to the canonical Rustickers source tree any more.

`.pi/skills/`

  Was a duplicate mirror of skill content. To clarify what was
  actually unique:
    .agents/skills/gpui-kit/                      (canonical)
    .agents/skills/gpui-kit-design-guides/        (canonical)
    .agents/skills/gpui-visual-check/             (canonical)
    .pi/skills/gpui-kit-design-guides/            (== .agents/ copy)
    .pi/skills/gpui-visual-check/                 (== .agents/ copy)

  Each file under .pi was byte-identical to its .agents sibling.
  Having two install roots meant refresh-skills operations had to
  touch two trees, and any drift between them produced diverging
  agent output for the same dev prompt. There is no scenario in
  this repo where .pi/skills adds content .agents/skills lacks.
  Remove.

`vendor/zed/`

  This was a hand-vendored bundle of Zed editor source (crates/
  docs/ extensions/ legal/ nix/ script/ tooling/ totalling 180 KB
  on disk) that was never tracked by git -- 'git ls-files vendor/'
  returned nothing. The directory only existed as a local
  reference scratch during the kit-migration design work; no
  rustickers source file imports from it, no README or FRONTEND
  paragraph depends on its presence, and it conflicted with the
  'clean and well organized' shape the repo is converging on.
  Delete the directory outright (rm -rf rather than git rm).

Doc sync:

  - FRONTEND.md repo layout: drop the 'examples/' row under
    src/, add a 'scripts/' row so the new screenshot.ps1 binary
    tool is discoverable. 'examples/' would be misleadingly
    listed as a directory but be empty after the example was
    removed in 0cb513e.
  - FRONTEND.md testing-gates block: drop the
    'cargo build --features ui --example clipboard_ux_smoke'
    line; the example no longer exists. The other four lines
    (fmt / check / clippy / test) are unchanged.
  - FRONTEND.md 'Visual flows' sentence: replace the stale
    'covered by clipboard_ux_smoke.rs' pointer with a pointer
    to .agents/skills/gpui-visual-check/ + scripts/screenshot.ps1,
    which is the actual shape that took its place.
  - FRONTEND.md skill catalog: replace '.pi/skills/...' line
    with a tree that mirrors the on-disk layout under
    .agents/skills/, and surface scripts/screenshot.ps1 next
    to skills-lock.json so the Windows capture tool is
    discoverable from the catalog table.
  - README.md testing gates: drop the now-invalid
    --example clipboard_ux_smoke build line and bump '212
    passed' to '213 passed' to match the count after the
    04592d3 (editor_save_writes_label_alongside_content)
    test was added.

The repo is now a single tree:

  src/  tests/  migrations/  scripts/
  Cargo.toml  Cargo.lock  build.rs
  assets/  packaging/  screenshots/        (project assets)
  .agents/  .github/  .vscode/             (tooling config)
  .cargo/  .gitignore                     (build config)
  README.md  ARCHITECTURE.md  FRONTEND.md  (docs)
  skills-lock.json                        (skill provenance)
  .tmp/                                   (gitignored scratch)

Verified: cargo test --locked --features cli reports 213 passed,
1 ignored (190 lib + 19 clipboard_storage + 3 sticker_palette + 1
sticker_placement). No Markdown file in the tree still references
.pi/, vendor/, or clipboard_ux_smoke.

The 'preserve all initially untracked files' constraint covered
.agents/, .pi/, and skills-lock.json; .pi is now intentionally
removed because its content is fully represented under .agents/.
If a downstream copy of this branch needs the duplicate mirror
to live in a per-host install dir (e.g. ~/.pi/skills/), that is
out of scope for the repo -- it's a per-machine install location.
binwenw and others added 28 commits October 8, 2026 20:47
README and FRONTEND describe the sidebar workspace, hover-trailing row actions, single-header title semantics, editable shortcut model, and the visual fixture workflow under tests/visual/. Drop the fixed-shortcut footnote and the Local by design tag in favor of the Settings-anchored architecture.
A copy of text that is already a saved sticker (plain or locked) no longer
materializes a duplicate clipboard_captures row. Stickers now carry a
text_digest of their plaintext, computed at seal/save/promote time, so the
capture path can find the matching saved identity and advance its recency
instead, consuming any existing duplicate capture and folding its activity
into the sticker. Ciphertext is never hashed: legacy locked rows gain a
digest only once an authorized edit, unlock, or rotation yields plaintext.
Startup backfills legacy plaintext digests and reconciles leftover captures.
Fixes the clippy items_after_test_module warning so the tree is lint-clean.
…agent contract

Replace ambiguous subcommands and hidden ambient behavior with an explicit
grammar that serves both humans and supervising agents.

Top-level:
  rst note [TEXT | --file PATH | --file -]      # Markdown notes from text or stream
  rst command [--shell SCRIPT] -- PROGRAM ARGS  # direct exec, no shell parsing
  rst list | show ID | result ID                # discover and inspect
  rst open ID | close ID                        # window state (desktop only)
  rst delete ID --yes                           # permanent removal
  rst view PATH                                 # transient file/folder/URL preview
  rst text list [--source/--state/--search]     # text entries
  rst text exec [--id SOURCE:ID --non-interactive] [--terminal] -- ARGS
  rst skill list | show | run [--set K=V] [--dry-run] | guide [--json]

Global: --json (one object), --data-dir PATH (isolated, offline), --offline.
Exit codes: 0 ok, 2 grammar/clap, 1 runtime; text exec propagates child status
(130 cancel, 126/127 launch failure).

Architecture:
- new Runtime{paths,offline} as the single seam for storage location and IPC
- Cli::try_parse_from cross-validates global + subcommand conflicts so placement
  before or after the subcommand is identical
- utils::command_line::split replaces winsplit: Windows-style quoting, preserves
  empty arguments, rejects NUL and unterminated quotes; shared between CLI and
  the desktop command runner so preview and execution parse identically
- New agent-guide.md documents the canonical recipe for supervising agents;
  parsed by tests/cli_docs.rs so the documentation never drifts
- AppPaths::default_location separates discovery from filesystem creation, so
  dry runs and JSON-only subcommands never touch %LOCALAPPDATA%

Verification:
- tests/cli_process.rs (8) — binary startup, exit codes, --data-dir isolation
- tests/cli_docs.rs (parser-tests every literal `rst …` in README + agent-guide)
- tests/skill_cli.rs (11) — black-box catalog, dry-run, --set validation, recipes
- tests/skill_execution.rs (12) — desktop command runner against recipe outputs
  (escaped selection is data; static code never embeds engines; hostile --set
  values stored verbatim)
- cargo test --features cli: 205 passed, 0 failed
- cargo clippy --features cli -- -D warnings: clean
- sub-agent audits validated chooser search, exit-code propagation, recipe
  isolation, JSON error shape, byte-exact Unicode output

No live user database was modified; all experiments ran under --data-dir.
Per-pixel alpha requires GPUI DirectComposition (DXGI_ALPHA_MODE_PREMULTIPLIED).
Leave the framework default enabled and honor external overrides. The previous
transitive fix reverted the auto-set of GPUI_DISABLE_DIRECT_COMPOSITION because
flipping it stripped WS_EX_NOREDIRECTIONBITMAP and broke transparent surfaces
across every window.

Component Root defaults to an opaque theme background, so sticker, manager,
selection popup and TXTs windows pass gpui::transparent_black() into Root
while retaining its overlay/focus facilities. The Markdown editor uses
.appearance(false) because .bordered(false) alone still paints the input
background. Window surface tints (manager background, selection background,
picker background) now run at WINDOW_SURFACE_OPACITY so text and controls
stay legible while the desktop shows through.

Manager sidebar holds its surface without an opaque sidebar token so its
pane participates in the window transparency. Section::Commands excludes
armed or selection-routed commands so they only appear in their Automation
destinations. Row-selected backgrounds are kept: the prior refactor that
folded two .hover calls into a single conditional closure is preserved.

Verified at the OS pixel level on Windows: before this fix, an isolated
Text sticker stayed opaque over both red and blue backdrops (delta 0).
After Root is cleared plus the editor appearance is disabled, the same
sticker showed (64,26,26) over red and (26,26,64) over blue (delta 76) in
both preview and editing modes. The selection popup and TXTs picker also
pass desktop-pixel checks. Manager/selection-input windows still have
opaque areas and are tracked separately.

The unrelated WebView rework from earlier this session is not in this
commit and stays deferred; embedded HTML previews remain broken until that
is fixed.
The selection popup used to drop its current text the moment the user
returned to the desktop — picking a command opened a sticker with it,
cancelling discarded it. The TXT picker cannot recover either case from
its own store, so the only escape hatch was "remember to copy before
you trigger the popup".

Both exit paths now record the popup's current text into the clipboard
collection so the TXT picker reminds the user where it went:

  - `open_selected` (Enter on a command sticker; also reached after the
    user types in the input view and presses Enter, because
    `confirm_input` calls `show_stickers(text, …)` before forwarding
    to `open_selected`)
  - `dismiss` (Escape and the inline close button)

The recorded payload is the "live" text the popup is acting on:

  - input view     -> `input.read(cx).value()` so a typed-but-cancelled
                       payload is preserved
  - chooser view   -> the captured selection, or the just-confirmed
                       typed value once `show_stickers` rebuilt the
                       chooser

The repository already dedupes by digest + text and bumps an existing
saved sticker's recency when its content matches, so empty/whitespace
text is the only payload we gate on. Recording runs even when
`clipboard_settings.capture_enabled` is off because the user explicitly
initiated the action; the picker reads `clipboard_captures` on its
existing 400 ms snapshot loop, so a new row shows up in TXT within a
tick. `logging` covers success (`debug`) and failure (`warn`).

Untracked for now: the OS Alt+F4 path closes the window via
`on_window_should_close` and bypasses `dismiss`, so pressing Alt+F4
still drops the text. The popup is frameless (`titlebar: None`), so
this only matters for keyboard power-users; flag if you want that path
hooked too.
The TXT picker's paste mode ships the chosen text to the user's last
foreground window via SetForegroundWindow + simulated Ctrl+V. That path
went through `native::clipboard::paste::platform::foreground` and its
peer `verify`, both of which bailed with "The original destination is
not focused. Use Copy only." when the destination's PID matched our
own.

Every native input inside rustickers is a window owned by the
rustickers process: the selection popup, sticker markdown editor, the
manager sidebar search, settings fields, etc. So any hotkey invocation
that captured the foreground HWND while the user was in one of those
inputs (the most common case for the TXT picker) tripped the self-pid
gate, `dispatch_content` returned `Err`, and the picker's status flipped
to "Could not use text. Check the password or paste destination." The
clipboard write and the simulated Ctrl+V both never ran, so the text
never made it into the input control.

Drop the self-pid gate from both helpers. The remaining liveness checks
(IsWindow, GetWindowThreadProcessId) still cancel the paste if the
destination closed in the interim, and the GetForegroundWindow()
strictness is preserved for cross-process destinations so we still
refuse to paste in front of the wrong app. Windows treats intra-process
foreground changes as low-risk and permits SetForegroundWindow on
windows owned by the calling process regardless of the foreground
lock; GPUs reattaches the last-focused child control when the window
regains foreground, so the Textarea / InputState stays the recipient
of the synthesised Ctrl+V.

Verified against the full lib test suite (98/98) and the paste +
watcher + hotkey modules specifically (6/6). The binary was rebuilt for
runtime confirmation; cross-process destinations behave exactly as
before this commit.
Windows' low-level keyboard hook can drop a key-up event during a
foreground transition (UAC prompt, lock screen, RDP focus swap, or a
process that consumes raw input before USER32). When that happens
mid-chord, the matcher's per-key `down` array stays pinned for the
virtual key the user already released, and the follow-up press reads
`repeat = self.down[ix] == true`, so the chord never fires again
until the app restarts.

Reset `down` when all chord modifiers are released: that is the
natural baseline where any next chord must be re-entered from
scratch, so dropping the phantom "still held" entry is safe.
`suppressed` is left intact so an unmatched release (chord key
released after every modifier) still keeps its trailing-release
suppression promise; the next chord match clears it.

Add a regression test that reproduces the missed V-up scenario and
verifies recovery, including subsequent plain V presses not being
suppressed.
Slot 1 (TXTs chooser) now binds Ctrl+Shift+V instead of
Ctrl+Alt+V. This keeps slot 2's modifier-only gesture (Ctrl+Alt for
quick file preview) unblocked while a TXT shortcut is firing, and
matches the more conventional Ctrl+Shift+V clipboard-history binding
familiar from other apps.

Existing users that already saved a hotkeys.json keep their bindings;
the new default only applies to fresh installs or after the file is
deleted. Update the matcher tests to express the per-chord second
modifier by code instead of an `alt` boolean so the helper stays
readable as the defaults evolve. Update the duplicate-detection
integration test to seed `shift+ctrl+v` against the new slot 1.
Slot 0 (Show manager) now binds Ctrl+Shift+R instead of Ctrl+Alt+R,
and slot 3 (TriggerSelectionToCommand) now binds Ctrl+Shift+Space
instead of Ctrl+Space. This keeps slot 2's modifier-only gesture
(Ctrl+Alt for quick file preview) unblocked across all three
chord slots and matches the more conventional Ctrl+Shift modifier
combination.

Existing users that already saved a hotkeys.json keep their bindings;
the new default only applies to fresh installs or after the file is
deleted. Update the matcher tests to keep step-3's pressed modifier
in sync (Shift instead of nothing), and pin the side-modifier test
to an explicit snapshot so its 'Ctrl+Shift is not Ctrl' guarantee
does not silently break as defaults evolve.
`d2e75d` removed the self-pid gate in `platform::foreground` so the TXT
chooser can paste into rustickers-owned windows. Same-process targets
worked but cross-process destinations like Notepad still failed: the
chooser's `deliver()` destroyed the picker window *before* the async
`dispatch_content` reached `SetForegroundWindow`, and once rustickers no
longer owned the foreground window User32 routed the call through the
new (or null) foreground process and the foreground lock bounced it
back. `verify` then saw `GetForegroundWindow() != handle` and the paste
silently aborted.

Switch the foreground synchronously on the GUI thread, while the picker
is still alive and rustickers owns the foreground lock. `dispatch_content`
now receives a `foreground_done` flag and skips its redundant call. Add
a `delivering` flag that gates `observe_window_activation` so the
imminent deactivation event from the same `SetForegroundWindow` does
not race with the explicit `window.remove_window()`.

`platform::foreground` also gets a brief `GetForegroundWindow` poll:
User32 sometimes applies the foreground change a few milliseconds after
`SetForegroundWindow` returns, and the chooser could otherwise call
`paste_foreground` (which still does an immediate verify) before the
transition settled.

Verified by running `cargo test --lib --features ui` (99/99) and
`cargo test --features cli --test text_repository` (12/12). Same-process
pastes (manager sidebar, sticker editor, settings fields) still work
because the `same_process` branch returns Ok immediately.
The earlier chord-recovery fix only helped when a non-modifier key-up
was dropped. A full stream interruption — Windows shell hotkey such
as Win+L, RDP focus swap, or a foreground app that consumes raw
input — can also strand held modifiers. The hook may not see their
release, the keys are still physically held from the matcher's point
of view, and every exact-match shortcut continues to be blocked even
after the user has let go of everything.

Add `Matcher::reset`: drop all held/suppressed/gesture state in one
shot so the next chord enters on a known-clean baseline. This is a
calibrated escape hatch — clearing `down` after every modifier
release also fires too eagerly; clearing on `waiting_for_release && !active`
misses the modifier-only case. Tests pin the new contract: a reset
discards pinned keys, in-flight reload-while-held waits, suppression,
and pending preview gestures without dispatching them.
The Windows low-level hook is silently removed once a callback runs
for more than 1 second, with no error surfaced back to the caller
(see LowLevelKeyboardProc). Before this change the listener still
held the stale handle and never reinstalled it, so every shortcut
could stop working after any 1-second stall (e.g. clipboard or text
work while the hook is held), after lock/unlock, or after an RDP
focus swap.

Lift hook ownership and shutdown into `windows_listener::Listener`:
own a hidden top-level window on the keyboard pump thread, register
for WTS session notifications, and pause on LOCK/CONSOLE_DISCONNECT/
REMOTE_DISCONNECT/SUSPEND. On UNLOCK/CONNECT/RESUME call
`Matcher::reset` and reinstall the hook. Window proc defers the
notifications to the outer pump so reentrant delivery never sees
the matcher lock held. A failed reinstall is retried via a
1-second WM_TIMER; pending retry is dropped whenever a new lock
arrives, so it cannot survive into a session that just locked again.

`cargo test --lib` reports 101 passed (+ 1 opt-in Windows desktop
test that consumes a tagged F24 probe through three lock/unlock
notification cycles and a forced retry), and `cargo check` is clean.
When reopening a sticker whose DB row is still StickerState::Close, drop
its per-monitor placement and open the window centered on the current
primary display, keeping only the saved size.

* New OpenOptions::preserve_size_only, set in open_with_detail_and_options
  when detail.state == StickerState::Close and consumed by open_with_options.
* forget_saved_placement strips placements, preferred_display_uuid,
  display_id/uuid and native_*/width/height while leaving width, height
  and virtual_desktop_id untouched.
* center_within maps a visible area and a window size to the centered
  top-left point, handling negative monitor origins and oversize windows.
* Skip the deferred apply_window_rect while preserve_size_only is set so
  the centered bounds are not overwritten by the saved rect.

Unit tests cover the strip path, the resolver returning None after the
strip, and the centering math for normal, negative-origin and oversize
cases.
Manager and locked-sticker opens now start with the cursor already in
the next action: the manager search box and the unlocked-sticker
view's Unlock button. Loading and busy labels stop pretending to be
ongoing actions with trailing dots, while input placeholders keep
the single Unicode ellipsis the design guide reserves for "more
input required".

Manager (src/native/windows/main/mod.rs)
* MainWindow::new() focuses the search InputState on construction,
  mirroring the TXTs picker so every manager launch is one keystroke
  away from filtering stickers.

Locked Markdown sticker (src/native/components/stickers/markdown.rs)
* New lock_focus: FocusHandle on MarkdownSticker; opened locked
  stickers move focus there so the first keypress is the next
  command rather than a placeholder tab stop.
* The locked-view body wraps with track_focus + on_key_down handler
  that routes Enter to request_unlock. The title-input wrapper's
  Enter -> save_title handler still wins while the title field is
  focused, so editing the title remains a single keystroke.

Ellipsis cleanup
Per the design guide, the trailing ellipsis is reserved for buttons
and menu items that open a dialog, sheet, or separate window, not
for status text. The following decorative dots are removed and a
few ASCII "..." placeholders are normalized to the single Unicode
ellipsis:

* Removing "..." / normalization removed on:
  - windows/main/settings.rs: "Saving...", "Checking protection..."
  - windows/main/collection.rs: "Loading stickers..."
  - windows/text/picker.rs: "Working...", "Loading texts..."
  - components/stickers/file/mod.rs: "Loading ..."
  - components/stickers/file/summary.rs: "Refreshing summaries..."
  - windows/selection.rs: "Looking for command stickers ..."

* Placeholders normalized to single Unicode "..." on:
  - windows/text/picker.rs: "Search texts ..."
  - windows/selection.rs: "Filter stickers ..." and
    "Type the text to send to a command sticker ..."

* Header label "Input and invoke ..." dropped entirely: it is a
  static explanation, not a button, so neither opening a dialog nor
  receiving more input applies.

Dialog-opener buttons (Unlock text, Remove encryption, Convert to
local file, Set/Change password, Master password, Rename, Lock,
Edit) keep their ellipses; each opens a dialog, sheet, or
separate window.
Each manager row now exposes a one-click handler for the action its
section is built around, alongside the existing delete button:

* Section::Scheduled rows: a Play-icon "Start now" button that
  triggers an out-of-band run of the armed command. The new
  `command::background::run_now` loads the latest sticker content
  from the store (so edits the user just made in the command window
  are honored) and runs it through the same path the scheduler uses,
  persisting the result through the standard `run_once` pipeline.
  The button is disabled while the command is currently running, so
  a duplicate tick is not kicked off by an impatient click.

* Section::Selection rows (only when the command accepts selection):
  a Forward-icon "Run with selection" button that pops up the
  selection routing dialog pre-populated with just this sticker.
  The chooser view confirms which command will run; pressing Enter
  continues into `StickerWindow::open_with_selection` exactly like
  the hotkey path. Errors while loading the full sticker detail
  surface through `MainWindow::error`, matching the delete flow.

Both buttons sit in the same hover-visible h_flex as Delete and use
the same keyboard hit testing harness (`stop_propagation` on
mouse_down and mouse_up) so they never collide with the row's
double-click-to-open handler.
The manager's Run-with-selection row action now opens the selection
popup pre-routed to the clicked sticker instead of going straight to
the chooser view, so the flow stages each action the user actually
takes:

* Popup comes up in the Input view with the cursor sitting in the
  text area, ready for either a typed selection or an empty one.
* Pressing Enter runs `confirm_input`. When the popup was launched
  from the manager, the picked sticker is honored and the chooser
  view is revealed with that single sticker filtered in; the typed
  text becomes the selection the sticker's window will receive. The
  pick sticks across `edit_selection` retries so an edit-confirm
  cycle keeps landing on the chosen sticker.

Implementation notes
* `SelectionPopupInit` gains an `InputPicked` variant carrying the
  pre-picked sticker; `SelectionPopup` keeps it as an `Option`
  field so the existing `Choose` and `Input` paths keep their
  default DB-wide resolve behaviour.
* `confirm_input` short-circuits the `resolve_selection_command` +
  `show_stickers` flow when `picked` is set, mirroring the
  existing Single-target branch but skipping the resolve call that
  would otherwise re-load every accept_selection sticker in the
  database.
* The existing `escape_dismiss_target_active` plumbing, focus
  routing and per-keystroke subscriptions remain unchanged; the
  only behavioural difference is where the chooser view sources its
  sticker list.

Verification
* cargo fmt --all -- --check
* cargo check --features ui / --no-default-features --features
  storage / --no-default-features --features cli --bin rst
* cargo clippy --features ui
* cargo test --features cli --lib: 225 passed.
@albertwoo
albertwoo merged commit 8070468 into main Oct 12, 2026
3 checks passed
@albertwoo
albertwoo deleted the feat/cli-redesign branch October 12, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant