You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Design spec for a running-windows switcher, built as three stages, each independently shippable:
Fallback header label — when the focused app has no layout match, the client header shows Home (xterm) instead of bare Home. No enumeration, no list. (Stage 1: fallback header label — Home (xterm) #123)
Running-programs list — tapping that header text opens a list of currently-running programs (only running ones — xterm appears even with no layout, disappears when closed), entries labeled by the layout they'd match with the raw program in parens on default fallback. Display only. (Stage 2: running-programs list UI & enumeration #120)
The map ends when the design decisions are resolved into build tickets ready to implement. Seeded by #20, which stays open as the raise-capability reference.
Notes
Domain: platform coupling (GNOME Shell extension in packaging/gnome-shell, daemon backends in daemon/deckd/platform.py), protocol (daemon/deckd/protocol.py, client/src/protocol.ts), layout model (daemon/deckd/layouts.py).
Committed early decisions: GNOME-only write side; chrome-internal raise (no raise: layout action primitive in v1); raise-only (no launch fallback).
Use /grilling + /domain-modeling for decision tickets; /prototype for the viability spike; /research for API fact-finding.
GNOME 50 drift: deckd-focus@local (stage-0 focus publishing) is currently dead on GNOME 50 — fix tracked in #125, independent of the switcher stages. Wayland dev loop needs a full logout/login per extension code change.
Decisions so far
GNOME Shell enumeration & activation API research — Meta/Shell APIs (get_window_actors, Meta.Window get_id/wm_class/title/workspace, window-created/unmanaging/notify::title, Main.activateWindow) are stable across GNOME 40-48 and in-process D-Bus export is the only Wayland path; low API risk, annual shell-version bump chore.
Stage 1: fallback header label — Home (xterm) — default-fallback layout shows Name (wm_class||app_id), raw; gated by an additive is_default: bool on the layout push (focused_app already present); suffix suppressed on identity/title matches, pinned views, and the auto-ignore hold. Build: #124.
Build stage 1: fallback header suffix Home (xterm) — landed is_default wire flag + client suffix (jonocodes/deckd@08dbb11). Server tracks _current_is_default against the focus-driven resolution (re-derived on reload; cleared on override); Session.push_current forces False for demo ?layout= pin and chrome select_view pin so neither leaks the underlying program. Client renders (wm_class||app_id) in heading, badge, and aria-live announcement. Auto-ignore hold verified — no deckd-identity push leaks through (no Home (deckd)). Stage 2 (Stage 2: running-programs list UI & enumeration #120) is the frontier.
Wayland raise viability spike — GO: Main.activateWindow reliably raises on GNOME 50 Wayland — cross-workspace (switches active workspace), multi-window (key by window id + MRU tie-break), minimized (un-minimizes), flatpak, and clean not-running no-op; all human-observed. Identity is three keys: wm_class (always present, primary), gtk_application_id (GTK apps only), sandboxed_app_id (flatpaks) — GNOME Shell enumeration & activation API research #118's gtk-app-id assumption corrected. Spike branch spike/117-wayland-raise. Surfaced #125 (focus ext dead on GNOME 50).
Window identity & matching semantics — per-window list (one row per window; per-app grouping deferred). window_id is the single opaque string handle across extension→daemon→client→tap→raise (client echoes, never parses; platform-neutral). MRU demotes to list sort order; multi-match moot. Daemon owns labels via the layout matcher (match→layout name, else raw wm_class, title last resort). Enumeration adds sandboxed_app_id to AppInfo and carries workspace/minimized — both unrendered in v1. Unblocks Stage 2: running-programs list UI & enumeration #120, Stage 3: switch on tap — raise: primitive spec #122.
Backend interface & failure behavior — GNOME backend interface for Stage 2: running-programs list UI & enumeration #120/Stage 3: switch on tap — raise: primitive spec #122. Enumerate: watch_windows() -> AsyncIterator[Sequence[WindowInfo]] (push, coalesced ~100ms; mirror of watch_active_app). WindowInfo is two-layer: identity + state (window_id, wm_class, gtk_application_id, sandboxed_app_id, title, workspace, minimized); the matched label is a separate wire-payload field the daemon produces — backend interface is free of layout-pipeline concerns. Decline contract: capabilities() -> frozenset[str] flag (default {"watch_active_app"}); base class implements watch_windows/raise_window as raise UnimplementedCapability(...) (new exception, distinct from FocusBackendUnavailable — different UX: install-hint vs. silent absence). Raise failure: re-validate the list (dead row drops as feedback) + emit raise_failed event on the diagnostic stream — no toast primitive. Capability-unsupported surfaces as the list being absent from the chrome.
Stage 2: running-programs list UI & enumeration — chrome view (select_view: "windows", ADR-0008 carve-out, ADR-0005 deferred). Extension adds ListWindows() -> s only (polled at ~100ms, no WindowsChanged signal). New RunningWindowsMessage daemon→client ({type: "running_windows", windows: [{window_id, label, icon | null}, ...]}), full snapshot per push, broadcast to every session (chrome_media precedent). Daemon owns labels (per-push, no cache) — matched layout's display_name, default fallback to raw wm_class||app_id||title. icon copied from the matched layout when present, null on default fallback (honest absence, not a generic glyph). Chrome-btn always rendered; view shows "unsupported on this platform" when capabilities() lacks watch_windows (mirrors media browser's "no players" empty state). Build: #126.
Stage 3: switch on tap — raise: primitive spec — chrome-internal only: RaiseWindowMessage { type: "raise_window", window_id: "..." } joins ClientMessage union as fire-and-forget; no raise: layout action primitive in v1. Extension owns id↔Meta.Window map, exposes RaiseWindow(s) → b on org.deckd.Focus via Main.activateWindow. Daemon dispatches to backend.raise_window(); raise_failed on EventMessage (Live diagnostic event stream and correlation IDs #73) on failure. View auto-closes on raise (client sends RaiseWindowMessage then ClearViewMessage). Static raise: layout buttons deferred to future fog. Build: #127.
Not yet specified
Per-app grouping of the switcher list (one row per program with a window sub-level, GNOME Alt+Tab style) — a display option layered on the per-window v1 if wanted
Rendering window state in the list: workspace grouping/headers and minimized dimming (fields already on the wire from Window identity & matching semantics #119, just not shown in v1)
KWin, macOS, and X11 raise/enumeration backends — deliberate follow-up effort once the GNOME-shaped v1 proves the interface; the backend interface is designed platform-neutral so these slot in later
Launch-or-raise fallback (spawning apps that aren't running)
Filtering/pinning/ordering preferences for the switcher list beyond the basics the spec settles
Destination
Design spec for a running-windows switcher, built as three stages, each independently shippable:
Home (xterm)instead of bareHome. No enumeration, no list. (Stage 1: fallback header label — Home (xterm) #123)raise:primitive underneath). (Stage 3: switch on tap — raise: primitive spec #122)The map ends when the design decisions are resolved into build tickets ready to implement. Seeded by #20, which stays open as the raise-capability reference.
Notes
packaging/gnome-shell, daemon backends indaemon/deckd/platform.py), protocol (daemon/deckd/protocol.py,client/src/protocol.ts), layout model (daemon/deckd/layouts.py).watch_active_app); enumeration arrives with stage 2 (Stage 2: running-programs list UI & enumeration #120 closed).raise:layout action primitive in v1); raise-only (no launch fallback)./grilling+/domain-modelingfor decision tickets;/prototypefor the viability spike;/researchfor API fact-finding.deckd-focus@local(stage-0 focus publishing) is currently dead on GNOME 50 — fix tracked in #125, independent of the switcher stages. Wayland dev loop needs a full logout/login per extension code change.Decisions so far
Name (wm_class||app_id), raw; gated by an additiveis_default: boolon the layout push (focused_appalready present); suffix suppressed on identity/title matches, pinned views, and the auto-ignore hold. Build: #124.is_defaultwire flag + client suffix (jonocodes/deckd@08dbb11). Server tracks_current_is_defaultagainst the focus-driven resolution (re-derived on reload; cleared on override);Session.push_currentforcesFalsefor demo?layout=pin and chromeselect_viewpin so neither leaks the underlying program. Client renders(wm_class||app_id)in heading, badge, and aria-live announcement. Auto-ignore hold verified — no deckd-identity push leaks through (noHome (deckd)). Stage 2 (Stage 2: running-programs list UI & enumeration #120) is the frontier.Main.activateWindowreliably raises on GNOME 50 Wayland — cross-workspace (switches active workspace), multi-window (key by window id + MRU tie-break), minimized (un-minimizes), flatpak, and clean not-running no-op; all human-observed. Identity is three keys:wm_class(always present, primary),gtk_application_id(GTK apps only),sandboxed_app_id(flatpaks) — GNOME Shell enumeration & activation API research #118's gtk-app-id assumption corrected. Spike branchspike/117-wayland-raise. Surfaced #125 (focus ext dead on GNOME 50).window_idis the single opaque string handle across extension→daemon→client→tap→raise (client echoes, never parses; platform-neutral). MRU demotes to list sort order; multi-match moot. Daemon owns labels via the layout matcher (match→layout name, else rawwm_class,titlelast resort). Enumeration addssandboxed_app_idtoAppInfoand carriesworkspace/minimized— both unrendered in v1. Unblocks Stage 2: running-programs list UI & enumeration #120, Stage 3: switch on tap — raise: primitive spec #122.watch_windows() -> AsyncIterator[Sequence[WindowInfo]](push, coalesced ~100ms; mirror ofwatch_active_app).WindowInfois two-layer: identity + state (window_id,wm_class,gtk_application_id,sandboxed_app_id,title,workspace,minimized); the matched label is a separate wire-payload field the daemon produces — backend interface is free of layout-pipeline concerns. Decline contract:capabilities() -> frozenset[str]flag (default{"watch_active_app"}); base class implementswatch_windows/raise_windowasraise UnimplementedCapability(...)(new exception, distinct fromFocusBackendUnavailable— different UX: install-hint vs. silent absence). Raise failure: re-validate the list (dead row drops as feedback) + emitraise_failedevent on the diagnostic stream — no toast primitive. Capability-unsupported surfaces as the list being absent from the chrome.select_view: "windows", ADR-0008 carve-out, ADR-0005 deferred). Extension addsListWindows() -> sonly (polled at ~100ms, noWindowsChangedsignal). NewRunningWindowsMessagedaemon→client ({type: "running_windows", windows: [{window_id, label, icon | null}, ...]}), full snapshot per push, broadcast to every session (chrome_media precedent). Daemon owns labels (per-push, no cache) — matched layout'sdisplay_name, default fallback to rawwm_class||app_id||title.iconcopied from the matched layout when present, null on default fallback (honest absence, not a generic glyph). Chrome-btn always rendered; view shows "unsupported on this platform" whencapabilities()lackswatch_windows(mirrors media browser's "no players" empty state). Build: #126.RaiseWindowMessage { type: "raise_window", window_id: "..." }joins ClientMessage union as fire-and-forget; noraise:layout action primitive in v1. Extension owns id↔Meta.Window map, exposesRaiseWindow(s) → bonorg.deckd.FocusviaMain.activateWindow. Daemon dispatches tobackend.raise_window();raise_failedon EventMessage (Live diagnostic event stream and correlation IDs #73) on failure. View auto-closes on raise (client sendsRaiseWindowMessagethenClearViewMessage). Staticraise:layout buttons deferred to future fog. Build: #127.Not yet specified
raise: <app>layout buttons — considered in Stage 3: switch on tap — raise: primitive spec #122, deferred to a follow-up effort once the switcher proves the chrome-internal raise pathOut of scope