Dated proposal · #916 · 2026-09-24 · hand-written and frozen, not a description of shipped behavior
Window titles and application names sit behind one consent (ADR-0032), off by default
and off until the user turns it on. Many users never find the Settings row, so list_windows
and describe_screen keep reporting anonymous rectangles, fidget://windows
stays empty, and the fidget knows where the windows are but not what they are. This proposes a hint
that closes that gap once, without ever reading as the product asking for more surveillance
permission.
Arm the hint with one of the three triggers #916 names, then switch between the three candidate surfaces. The dismissal is real: Quit and relaunch reloads the stage from persistence, and only Reset everything brings the hint back.
The fidget knows where your windows are, not what they are. One switch in Settings turns on titles and application names together.
I know where your windows are, not what they are. Want me to open Settings?
macOS Screen Recording lets your fidget read window titles. Fidget does not capture the screen. One switch covers titles and application names alike, so with it off fidget knows where the windows are and not what they are; the sprite lands on them either way. With it on, list_windows, describe_screen and the readonly MCP resource report the owning application and the title.
The operating system's own permission dialog comes from this checkbox, and from nothing else. It is not mocked here, and the hint never triggers it.
Nothing has asked for a name yet. NamesHint::Quiet.
list_windows
{ "bounds": [22,52,300,168], "owner": null, "title": null }
{ "bounds": [150,132,300,150], "owner": null, "title": null }
{ "bounds": [70,250,250,120], "owner": null, "title": null }
One derived value, not three booleans that have to stay in step. The hint is on screen because the state says so, and the state has exactly one reason to be what it is.
| State | What produced it | What the user sees |
|---|---|---|
| Quiet | Names are usable, or no nameless read is outstanding. | Nothing. |
| Due | Something asked for windows and got them nameless while the consent was off. | The hint, once, in whichever variant ships. |
| Dismissed | The user said no. | Nothing, now or after a restart. The Settings row stays where it is. |
/// Why the hint is or is not on screen. One value, derived — never three
/// booleans that have to stay in step.
enum NamesHint {
/// Names are usable, or no nameless read is outstanding.
Quiet,
/// Something asked for windows and got them nameless while the consent was off.
Due,
/// The user said no. Persisted, so it survives a restart.
Dismissed,
}
usable(WindowNames) runs in three places on main: the
can_read_titles closure in platform::window_source()
(src-tauri/src/platform.rs:770), the frontmost-name gate in the frame loop
(src-tauri/src/frame_loop.rs:1118), and platform::list_window_titles()
(src-tauri/src/platform.rs:722). The first two run every frame and every sense interval
whether anyone wants a name or not, so a miss recorded there would arm the hint seconds after every
launch. Only the third is a request, the fidget://windows read from
mcp_http::read_resource. list_windows and describe_screen never
call usable at all; they read the frame loop's snapshot, whose windows carry
owner: None, title: None under the same consent (#979). So Due is recorded at
the two request seams, the resource read and the frame loop's tool dispatch
(src-tauri/src/frame_loop.rs:2113) for those two tools, in a process-global atomic beside WANT_WINDOW_NAMES in src-tauri/src/consent.rs. A flag rather
than a channel because the resource read runs on the MCP HTTP thread with no AppHandle
and no route to the UI.AppHandle and already
does exactly this for the first-run tour at src-tauri/src/frame_loop.rs:1072 and
:1099.Dismissed persists as one new bool on Settings, the twin
of first_run_tour_shown at src-tauri/src/settings.rs:1871 — no
BoolField, no FormRow, set and saved where it is shown.!do_not_disturb, the same gate the first-run tour takes.The hint never names macOS Screen Recording, never names ScreenCast, never says screenshot, and never reaches for a verb of sight — a fidget that sees is a fidget that captures, whatever the next clause says. It names what the fidget knows and what it does not — where the windows are, not what they are — in the words the Settings row's own disclosure already uses, and the row names the grant one line down, which is where #888 and #979 put it and where a disclosure belongs. A hint that leads with the permission is asking for the permission; a hint that leads with the capability is describing the fidget.
One consent, one thing it gates. Before ADR-0032 the copy had to explain a split — application names free, titles gated — and that split is gone, so the hint no longer says what the fidget still knows without consent, because without it the fidget knows no name at all. The same product rule holds on every OS. Only the platform label under the Settings row changes: Screen Recording on macOS, ScreenCast on Linux, no system permission on Windows.
| Alternative | The case for it | Verdict |
|---|---|---|
| A Settings banner alone — surface the offer inside the Privacy pane and nowhere else. | Zero new surfaces, zero new copy, and it cannot interrupt anyone. | Rejected. The user who needs the hint is the user who never opens Settings. |
Append a nudge to the fidget://windows resource text so the agent relays it. |
One string, no UI at all, and it reaches every Harness for free. | Rejected. The copy then goes through a language model, so "never foreshadows Capture" stops being something the repository can hold. |
| Repeat the hint on every nameless read. | Nobody misses it, and a user who ignored it once gets another chance. | Rejected as a nag. An agent can read windows many times a minute; the second showing is already an interruption. |
| Make names on by default. | No hint needed, because there is no gap to close. | Rejected. It is the issue's own out-of-scope line, and it breaks the one consent rule that holds identically on every OS. |