Repository navigation
Feat/cli redesign - #9
Merged
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.