···237237238238---
239239240240+## Window placement
241241+242242+**Owns:** where every BrowserWindow lands when it's opened, reused, or
243243+when display topology changes.
244244+**Lives in:** [`backend/electron/window-placement.ts`](../backend/electron/window-placement.ts) — a pure module with no `screen.*` or `BrowserWindow.*` calls inside; the caller passes display state in.
245245+**Doc:** [docs/window-placement-refactor.md](./window-placement-refactor.md) (sprint plan and rationale)
246246+247247+A `Placement` discriminated union (`centered`, `cursor-display-fallback`,
248248+`edge`, `parent-centered`, `manual`) captures the *intent* for each
249249+window. Intent is recorded once at window-open time on
250250+`windowRegistry.params.placement` and never re-derived from flag soup
251251+(`center: true`, explicit `x/y`, `useCanvas`, etc.). All consumers — the
252252+URL-reuse and keepLive-reuse paths in `ipc.ts`, the fresh-open fallback,
253253+the display-watcher second pass, and the v2-tile open path in
254254+`tile-ipc.ts` — call the same `computePlacement(input)` and apply its
255255+`reposition` / `no-change` result. Stranded-rescue (<50% area on any
256256+display) is built in for every mode. Unit-tested with synthetic display
257257+layouts via a `fakeDisplays(...)` helper so display-switch regressions
258258+are catchable as single failing tests, no real multi-monitor hardware
259259+required.
260260+261261+---
262262+240263## Peek tiles vs webtil.es / DASL Tiles
241264242265The [DASL Tiles specification](https://dasl.ing/tiles.html) (the spec
+15-1
docs/tasks.md
···8899---
10101111+## Migration
1212+1313+- [ ] **Migrate tasks.md to Peek notes via MCP.** This `docs/tasks.md` file is being replaced by Peek itself: each task becomes a note (item) created via `mcp__peek__create_item`, tagged `#todo` `#peek` `#desktop`. Once migrated, the queue lives in Peek (queryable via `mcp__peek__list_items` filtered by tag), gets edited via `mcp__peek__update_item`, and Claude collaborates on a task note by opening it in Peek. After cutover, prune the migrated tasks here and eventually delete this file. See the companion `peek-task-queue` skill for the day-to-day workflow.
1414+1515+---
1616+1117## State machines
12181319- [x] **Page-host FSM — Phase 3: replace flag reads with FSM queries.** Shipped 2026-04-27. All 16 `isDragging`, 5 `isResizing`, and 17 `isMaximized` reads converted to `inDragging()` / `inResizing()` / `inMaximized()` predicate helpers (which read `fsmState` directly); flag declarations + inline writes deleted. `applyFsmEffect.SET_BODY_MAXIMIZED` no longer mirrors state to a shadow var. `toggleMaximize` and `startDrag` restructured so the FSM dispatch happens BEFORE `updatePositions`/`updateUrlParams`/`setBounds`, with pre-dispatch state captured via `wasMaximized = inMaximized()` where the post-dispatch branches need it. Pre-maximize bounds (`preMaximizeBounds`, `preMaximizeWindowBounds`, `dragOutOfMaximizeWindowSize`) remain module-scoped — moving them into FSM context is deferred (would require an attribute bag in the FSM module + larger refactor; current form is correct + tested).
···29353036- [ ] **Tauri: rename `pubsub:ext:*` startup topics to `pubsub:feature:*`.** `backend/tauri/src-tauri/src/lib.rs` still emits `pubsub:ext:startup:phase` and `pubsub:ext:all-loaded`. Electron side renamed 2026-04-24; Tauri/mobile is on hold per the v1-removal plan, but track this so it lands when Tauri is unfrozen.
3137- [ ] **Migrate bootstrap IPC channels to tile:lifecycle:* namespace.** `session-restore-pending` and `frontend-ready` in `backend/electron/entry.ts` are bare `ipcMain.handle()` registrations from before the tile system initializes. They're only reachable from trustedBuiltin renderers via `api.invoke()`, so the security gap is theoretical, but they're the last bare main-process IPC handlers outside `tile-ipc-gate.ts`. Decision skipped 2026-04-24: leaving them bare for now since they exist *before* any tile loads — the indirection through tile-ipc-gate would require either bootstrapping the gate earlier or having two IPC modes (bootstrap vs. post-init). Worth revisiting if tile-ipc-gate ever gains a "no token required for these specific channels" mode, or if entry.ts gets folded into a tile lifecycle.
3232-- [ ] **Page host window jumps to wrong position after switching primary monitor.** Surfaced 2026-04-24. Repro: connect external monitor, set it primary; open page host (cmd+L); disconnect/swap so the laptop becomes primary; cmd+L again — the whole window shifts to a different (off-screen / wrong-display) position. **Investigation 2026-04-27**: existing `backend/electron/display-watcher.ts` (initialized in `entry.ts:104`) handles already-orphaned windows (rescue if <30% overlap with any display), and `ipc.ts:754` / `ipc.ts:792` use `screen.getDisplayNearestPoint(getCursorScreenPoint())` for fresh window opens — so the obvious code paths look correct. Repro requires multi-monitor hardware. Next steps: (a) add diagnostic logging in the page-host open path that captures the cursor display, current display set, computed bounds, and any workspace-key collision at the moment of open, so the next user repro yields data; (b) investigate whether the cmd-panel-spawned page-host inherits a parent-window-relative position from a since-disappeared display via the cmd panel's own bounds; (c) check `key`-based window dedup logic — if the second cmd+L matches a stale workspace key from the previous display, restoring its OLD bounds would explain the wrong position.
3838+- [x] **Page host window jumps to wrong position after switching primary monitor.** Surfaced 2026-04-24; repro: connect external monitor as primary, open page host (cmd+L), then disconnect/swap so the laptop becomes primary and cmd+L again — page-host appeared off-screen or on the wrong display. Shipped 2026-04-28 by the `window-placement` refactor sprint (Phases 1-6, see `docs/window-placement-refactor.md`): introduced pure `backend/electron/window-placement.ts` module with `computePlacement` + `Placement` discriminated union, recorded a `placement` intent on every window at registration time (never re-derived), and rewired the URL-reuse / keepLive-reuse paths, the fresh-open fallback, the display-watcher second pass, and the v2-tile path to all consult the same function. Stranded-rescue (<50% area on any display) is built into the pure module and replaces the earlier `isWindowAccessibleNow` + `repositionOnCursorDisplay` helpers, which are deleted. The "wrong display, both still exist" case now resolves correctly because page-host's `cursor-display-fallback` placement causes display-watcher to re-evaluate against current cursor display. Unit-tested with synthetic display layouts so the regression class is single-failing-test catchable in future.
3939+- [x] **Slides anchor to stale window coordinates after display switch.** Surfaced 2026-04-28; slides reused cached coordinates from a prior display after a monitor connect/disconnect/primary-swap, opening off-screen or clipped. Shipped 2026-04-28 by the same `window-placement` refactor sprint: slides now pass `screenEdge` semantics (no x/y math in the renderer), main process records `placement: { mode: 'edge', edge }`, and `computePlacement` resolves coords against the actual cursor display every show. The renderer no longer reads `window.screen.width/height`. The stale-coordinates and "wrong display, both still exist" cases are both handled because every display change triggers a generic `computePlacement` pass via display-watcher.
3340- [ ] **Proton Pass extension doesn't autofill.** Still broken — doesn't show up in form fields, no autofill suggestions. Likely a content-script injection or messaging API gap in the webview/canvas setup. Reproduce: install Proton Pass extension, navigate to any login page, observe no inline autofill UI. (2026-04-17: agent triaged, 5 hypotheses in agent-ae11e03d worktree — extension gitignored so couldn't repro.)
4141+- [ ] **Proton Pass: permissions denied when configuring the extension.** Surfaced 2026-04-28. Trying to configure Proton in its options/popup page → permissions are being denied (likely OAuth/storage/host-permission requests fired from inside the extension UI). Probably interacts with the new `permission-handler.ts` policy + stored-decision flow shipped on 2026-04-27 — chrome-extension:// origins SHOULD be hard-allowed at `permission-policy.ts:82` (`if (url.startsWith('chrome-extension://')) return 'allow'`). Investigate: (a) what URL/origin the request actually arrives with (extension popup may run under a wrapped peek:// URL or its own chrome-extension:// origin); (b) whether permission-handler is even on the request path for extension-internal requests, or whether it's a separate chrome.permissions.request flow that needs its own handler; (c) whether the denial is on the Electron permission side or upstream in chrome-extensions.ts capability gating.
4242+- [ ] **Hidden tags framework — `from:{...}` tags are internal and should be treated that way.** Surfaced 2026-04-28. Some tags are bookkeeping (`from:rss-feed`, `from:share-extension`, `from:import`, etc.) — they're tracked for provenance/debugging but pollute the user-facing tag list, autocomplete, and the Tags UI. We need a generalized notion of "hidden" tags. Sketch: (a) a naming convention or a tag-metadata flag (`hidden: true`) that excludes a tag from default surfaces (Tags page list, cmd autocomplete, Settings → Tags); (b) a way for users to surface them on demand — a special search operator like `tag:hidden` or `#:from:rss` (explicit prefix opt-in), or a "Show hidden tags" toggle in the Tags UI; (c) decide whether existing convention tags (`from:*`, `source:*`, `_internal:*`) all auto-qualify as hidden, or whether each tag explicitly opts in. Out-of-scope until designed. Touches: `backend/electron/tag.ts` (queries), the tag noun in `features/cmd/nouns.js`, Tags page widget, Settings → Tags.
4343+4444+- [ ] **Tag-search window can't be opened multiple times — second `#tag` invocation focuses the existing window instead of running a new search.** Surfaced 2026-04-28. Repro: open cmd, type `#tagA`, hit enter — search window opens with results. Open cmd again, type `#tagB`, hit enter — instead of opening a second tag-search window (or running `#tagB` in the existing one), the previous `#tagA` window is just focused and the new query is dropped. Two acceptable shapes: (a) **multi-instance** — every `#tag` invocation gets its own window, so users can compare tag results side-by-side; or (b) **single-instance, re-query** — the existing window stays the singleton, but the new tag query replaces its contents. Likely root cause: tag-search tile is opened via `api.window.open()` with a fixed `key` (or `workspaceKey`), so the keepLive-reuse path in `backend/electron/ipc.ts` matches the existing window and short-circuits. Decide which shape we want, then either (a) drop the key / make it query-derived (`tag-search:${tagName}`), or (b) keep the key but plumb the new query into the reused window via a `tile:lifecycle:visible`-style IPC so the renderer re-runs the search on reuse.
4545+4646+- [ ] **Extension settings/popup links missing in Settings → Extensions (regression, again).** Surfaced 2026-04-28. Settings app's Extensions section is no longer showing the per-extension Settings/Popup links. Previously fixed 2026-04-23 ("Bundled-extension links in page host widget verified working — was failing on missing `api.adblocker.getStatus()`") and 2026-04-17 ("commands for browser-extension options pages"). Investigate what regressed since: (a) check `getChromeExtensionUiEntries` in `backend/electron/chrome-extensions.ts:491` returns popup + options entries; (b) check Settings → Extensions renderer reads them; (c) check tile-preload capability for extension UI introspection wasn't dropped during recent tile-architecture work. Likely a capability/IPC surface that's been re-narrowed.
34473548_(Tag-action toggles entry pruned 2026-04-27 — fully shipped + Playwright coverage in `tests/desktop/tag-actions-toggles.spec.ts` (4/4). See pruned log below for the one-liner.)_
3649···4154- [ ] **Update README + docs with tiles model, state machines, and webtil.es comparison.** README + DEVELOPMENT.md still reflect the pre-tiles era. Need: (1) a tiles-as-feature-unit section explaining capability-gated `tile-preload.cts` + manifest grants + per-tile BrowserWindow, with the project_tile_is_fundamental.md framing; (2) a state-machines section covering the three FSMs we ship — pubsub (`docs/pubsub-state-machine.md`), cmd panel (`docs/cmd-state-machine.md`), tile lifecycle (`backend/electron/tile-lifecycle.ts`), with a 1-paragraph "what they replace" each; (3) a comparison table: Peek tiles vs the [webtil.es](https://webtil.es) spec — where they overlap, where Peek goes further, where webtil.es is more conservative or has terminology we should adopt. Pull in `docs/tiles-single-file.md` and `docs/tile-preload-trimming-plan.md` as references rather than duplicating. Audience: contributors landing on the repo cold.
4255- [ ] **Design: ordered tag-backed lists feature.** The existing `list` feature is mainly used to view subsets of tagged items (e.g. `#todo #today`); keep that as one mode. Expand to a first-class lists primitive: each list = items sharing a `list:{name}` tag (prefix configurable in settings), with explicit ordering metadata so items can be reordered/pinned within the list. Surface as a noun in the cmd panel: `cmd → new list` (creates the tag + opens the list view), `list {name}` (open), `lists` (browse). Open questions: where ordering lives (per-item metadata? sidecar tag-metadata JSON? dedicated table?), how reorder propagates over sync, whether a list can layer additional filter tags, what happens when the prefix is changed retroactively. Out-of-scope until designed.
4356- [ ] **Add "Activate" button for extensions with `browser_action`/`action` but no popup.** TWP-class extensions (Translate Web Pages) declare a `browser_action` without `default_popup` — clicking the toolbar icon fires `chrome.action.onClicked`, which the extension uses to inject its UI via content script. Today's panel only shows buttons for popup OR options entries, so TWP shows only "Settings" with no way to *activate* TWP from the panel. Plan: (1) add new `action` entry type in `getChromeExtensionUiEntries` (`backend/electron/chrome-extensions.ts:491`) for extensions with `browser_action`/`action` declared but no `default_popup`; (2) add a `tile:chrome-extensions:dispatchAction` IPC (verify Electron supports firing `chrome.action.onClicked` programmatically — may need `chrome.scripting.executeScript` workaround if not); (3) extend `loadExtensionsPanel` in `app/page/page.js:3683` to render an "Activate" button for action entries. Surfaced 2026-04-23 — user expected this for TWP but it's a feature addition (not a regression restore).
5757+- [ ] **Page widget: per-page permissions panel.** Surfaced 2026-04-28. Add a side widget on page-host (alongside page info / tags / notes / entities) that lists the permissions this origin has requested, with for each: state (allowed / denied / prompt), persistence (one-shot vs remembered), and an on/off toggle to flip the stored decision live. Pulls from the same store as Settings → Permissions (`backend/electron/permission-store.ts` + `permission-policy.ts:resolveWithStored`). Toggle should write through `setDecision()` and publish a refresh event so Settings + this widget stay in sync. Open questions: (a) does the widget render only requested permissions for the current origin, or all known + a "request" button for unrequested? (b) for one-shot prompts that haven't been remembered, should the widget surface "remember" as a separate affordance? (c) renders only on real web origins (skip chrome-extension:// and peek://). Pairs with the Settings → Permissions UI shipped 2026-04-27 — same data model, page-local scope.
44584559---
4660