experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs(tasks): migrate active task queue into Peek; tasks.md becomes a pointer

The 16 active engineering todos in docs/tasks.md migrated to Peek items
(via mcp__peek__create_item) with markdown bodies and namespaced peek:*
machine tags (peek:todo plus domain peek:desktop / peek:mobile / peek:tauri
/ peek:atproto / peek:extensions plus type peek:bug / peek:feature /
peek:test-coverage / peek:docs / peek:design / peek:security / peek:deferred
/ peek:tile-arch).

Also retagged ~245 existing #peek items: bare todo / desktop / mobile /
daily-use tags replaced with their peek:* equivalents (~750 untag+tag
mutations). The dail-use typo (1 item) was normalized to peek:daily-use.
Bare #peek left untouched as the project membership marker, in advance
of project workspaces.

The active queue now lives in Peek; query peek:todo (intersect with
narrower peek:* tags as needed). docs/tasks.md retained as a one-page
pointer.

+5 -108
+5 -108
docs/tasks.md
··· 1 1 # Peek Task Queue 2 2 3 - Cross-session task list. Updated during Claude Code sessions, persisted via jj. When starting a session, scan this file for `[ ]` items and TaskCreate the relevant ones into the session. 3 + The active task queue lives in **Peek itself**, not in this file. 4 4 5 - Format: `[ ]` pending · `[>]` in-progress in some session · `[x]` done (can be pruned periodically) 5 + Query: items tagged `peek:todo` (intersect with `peek:desktop` / `peek:bug` / `peek:feature` / `peek:test-coverage` / etc. to narrow). 6 6 7 - Long-term replacement: once the tiles runtime ships with MCP server, this file becomes obsolete — tasks live as tagged items in Peek. 7 + - MCP: `mcp__peek__list_items({tags: ["peek:todo"]})` 8 + - Cmd panel: `#peek:todo` (or `#peek:todo #peek:desktop` etc.) 8 9 9 - --- 10 - 11 - ## Migration 12 - 13 - - [ ] **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. 14 - 15 - --- 16 - 17 - ## State machines 18 - 19 - - [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). 20 - * Test results: unit 628/628; page-host-fsm 5/5; session-restore-page-host 2/2; reopen-closed-window 5/5; page-load-failure 5/5. 21 - 22 - --- 23 - 24 - ## Tile architecture cleanup 25 - 26 - - [ ] **(deferred) `tags` bg+window consolidation.** 2026-04-27 attempt deferred: `tags/home.js` is 1641 lines and pulls in CodeMirror + Vim, so making it `resident: true` just to register commands would impose a real startup cost. Re-reading the audit, the topics `tags/background.js` publishes (`editor:changed`, `editor:add`) go to the **editor** feature, not its own home — there is no actual bg↔home round-trip inside tags to eliminate. The lazy bg tile is doing exactly what lazy was designed for. Revisit only if the lazy bg lifecycle becomes a real maintenance burden. 27 - 28 - --- 29 - 30 - ## Bugs 31 - 32 - - [x] **Web permission requests — feature complete (Phases 1-5 + extended polish).** All shipped 2026-04-27. **Phase 5 + extended polish:** favicon next to origin in both the prompt overlay and Settings → Permissions row (Google s2 favicon service + globe-emoji SVG fallback). Multi-prompt queueing UX (one prompt at a time, "+N more pending" badge, resolution advances queue). Allow+Remember + Deny+Remember test coverage. Settings rows grouped/sorted by origin so same-site decisions cluster. Per-permission emoji glyphs (📍 geolocation / 🎤 media / 🎹 midi / 📺 display-capture / 🔔 notifications / 📋 clipboard / 🖱️ pointerLock / ⤢ fullscreen / 🔗 openExternal) shared between prompt + settings. Live `Permissions (N)` count badge on settings sidebar. Relative timestamps ("3 hours ago"). Pure `resolveWithStored` extracted from permission-handler — full unit coverage of the policy-vs-stored-decision invariant ("stored decisions only override 'prompt'", never override hard allow/deny). Test coverage: `tests/unit/permission-handler.test.js` 22/22, `tests/desktop/permission-prompt.spec.ts` 6/6, `tests/desktop/permissions-settings.spec.ts` 6/6. Future work: per-origin "Revoke all" affordance (UX), per-permission default-policy override in settings ("always deny notifications by default"), live pubsub refresh of settings when prompts elsewhere persist new decisions. 33 - 34 - - [x] **Server-not-found / page-load failure shows blank white page forever.** User-reported 2026-04-27 with `http://www.metikmusic.com/`. DNS-failure / connection-refused paths were covered earlier via `did-fail-load` + `.load-error-overlay`. The remaining gap (and the actual repro) was hung loads — server accepts the connection, never responds. The 30s loading-timeout safety net silently flipped the spinner off and left the user staring at a blank page. Fixed 2026-04-27: timeout now surfaces the same `.load-error-overlay` ("The page took too long to load.") with the in-flight URL (read from `latestNavigationUrl`, not `webview.getURL()` which returns the previously-displayed URL during a pending navigation). Loading timeout is now overridable via `window.__loadingTimeoutMs` so the Playwright spec can fire the path in 1.5s instead of 30s. Test: `tests/desktop/page-load-failure.spec.ts` "Hung load: …" (6/6, was 5/5). Future polish (HTTP 4xx/5xx, TLS errors, certificate-error on session) tracked separately if/when reported. 35 - 36 - - [ ] **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. 37 - - [ ] **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. 38 - - [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. 39 - - [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. 40 - - [ ] **Playwright coverage for IZUI transient-on-blur auto-hide.** Surfaced 2026-04-28 with the regression that left slides + other panel-style transients accumulating on screen because macOS NSPanels don't fire `BrowserWindow.on('blur')` on intra-app focus shifts. Fixed in commit `091e2a43` by adding a main-process `win.on('focus')` loop that hides/closes other visible windows whose role is in `TRANSIENT_ROLES` (palette / quick-view / overlay), but **no e2e test asserts the behavior**. Spec to add (`tests/desktop/transient-on-blur.spec.ts` or extend `window-url.spec.ts`): open a `palette` (cmd panel) and a `quick-view` (e.g., a slide or settings), then open another `palette` / `quick-view` — assert the first one is no longer visible. Variants: focus moving to a `utility` window must NOT close transients (chain popup invariant); focus to a `workspace` / `content` window MUST close them. Hard to do with NSPanels and Playwright's focus model — may need to drive focus via main-process IPC rather than synthetic events. Likely the single biggest regression-risk surface in window management going forward. 41 - 42 - - [ ] **Playwright coverage for the slides feature (no spec exists today).** The whole slides feature has zero e2e coverage — surfaced 2026-04-28 when both a placement regression (slides anchored to stale `window.screen` coords) and a focus regression (slides not auto-closing on blur) shipped without tests catching either. Plan: a `tests/desktop/slides.spec.ts` covering the four edge anchors (Up/Down/Left/Right) opening at the expected screen edge, the close-on-blur invariant when another transient takes focus, and the `key=address:edge` identity contract (re-invoking the same slide reuses the window; opening a different edge gets a different window). The recent fix `f010a550` (skip URL-reuse when caller provides explicit `key`) is now covered by `window-url.spec.ts:120` — but the slides-specific user-facing flow ("Option+arrow opens slide on edge X") is still untested. 43 - 44 - - [ ] **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.) 45 - - [ ] **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. 46 - - [ ] **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. 47 - 48 - - [ ] **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. 49 - 50 - - [ ] **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. 51 - 52 - _(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.)_ 53 - 54 - --- 55 - 56 - ## Features 57 - 58 - - [ ] **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. 59 - - [ ] **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. 60 - - [ ] **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). 61 - - [ ] **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. 62 - 63 - --- 64 - 65 - ## Deferred from audits / fixes 66 - 67 - ### Atproto install flow hardening (from atproto pipeline audit, 2026-04-16) 68 - - [ ] **Verify atproto record signatures.** (Gap found 2026-04-20.) `backend/electron/atproto-source.ts:356` fetches the release record via `com.atproto.repo.getRecord` and accepts the response with no signature check. A compromised or malicious PDS can forge a record with CIDs pointing at tampered bundle blobs, and `verifyBlobCid()` (line 502) will happily validate those tampered bytes against the forged CIDs. Current trust boundary is TLS to the PDS, not the publisher's signing key. Full design and step-by-step plan (commit-level signature, MST inclusion, `@atproto/crypto`, PLC key-rotation handling, icon/screenshot gap) in [atproto-signature-verification-plan.md](atproto-signature-verification-plan.md). ~3 days. 69 - 70 - --- 71 - 72 - ## Planned major work 73 - 74 - ### Features-manager atproto plan phases (from `features/features-manager/PLAN.md`) 75 - - [ ] Phase 7 — polish + indexing (curated directory, relay/indexer service as separate repo) 76 - 77 - --- 78 - 79 - ## Pruned / completed log 80 - 81 - Keep short — for recent context only. Prune after a few weeks. 82 - 83 - - 2026-04-27 **Page-host FSM Phase 2 (drag/resize states).** Extended `app/page/page-fsm.js` with DRAGGING / RESIZING / DRAGGING_OUT_OF_MAXIMIZED states and DRAG_START / DRAG_MOVE / DRAG_END / RESIZE_START / RESIZE_MOVE / RESIZE_END events. Effect descriptors for SHOW_DRAG_OVERLAY / HIDE_DRAG_OVERLAY / EXIT_MAXIMIZED_FOR_DRAG. Transition table covers the drag-out-of-maximize flow (MAXIMIZED + DRAG_START → DRAGGING_OUT_OF_MAXIMIZED → NORMAL — never re-enters MAXIMIZED). Tolerated input-race no-ops on duplicate DRAG_START / RESIZE_START / INIT_COMPLETE so the unit warnings don't flood from a real-world burst. Unit tests grew from 15 to 33 (`tests/unit/page-fsm.test.js` 33/33). New `tests/desktop/page-host-fsm.spec.ts` (5/5) verifies the runtime adapter actually dispatches at each entry/exit point — round-trips for drag, resize, maximize, drag-out-of-maximize via real DOM event simulation; `window.__pageFsmState` exposes state for introspection. Page.js still uses its own `isDragging` / `isResizing` flags — the FSM is shadow state for now (Phase 3 replaces flag reads with state queries). 84 - - 2026-04-27 **Page-host FSM Phase 1 (maximize lifecycle).** New `app/page/page-fsm.js` — pure module, no DOM/IPC; states `INITIALIZING / NORMAL / MAXIMIZED`; events `INIT_COMPLETE / TOGGLE_MAXIMIZE`; effect descriptors for body class / URL param / handle visibility / pre-maximize capture/restore. Unit tests in `tests/unit/page-fsm.test.js` (15/15) cover every legal edge + illegal-transition warnings + end-to-end runs. Wired into `page.js` via `dispatchFsm` / `applyFsmEffect` — init dispatches `INIT_COMPLETE` with the `maximized` URL param so a restored maximized page-host transitions straight into MAXIMIZED. `toggleMaximize` dispatches to keep `fsmState` in sync (DOM mutations stay in toggleMaximize for now; effects are idempotent). Drag/resize/etc still own their own flags — Phase 2 will fold them in. Maximize state propagated through the four pinch points: `updateUrlParams` (writes `maximized=1`), session-save reads the URL param into `WindowDescriptor.params.maximized`, undo-close `ClosedWindowEntry.maximized`, `window-open` reads `options.maximized` and (a) snaps the BrowserWindow to the active display's work area instead of adding canvas margins, (b) propagates `maximized=1` to the new page-host URL params. Test-bypass helpers added to `saveSessionSnapshot`/`restoreSessionSnapshot` (`_forceForTest` skips `isTestProfile()` + `isVisible()` guards) and exposed via `__peek_test.forceSaveSession`/`forceRestoreSession`. New `tests/desktop/session-restore-page-host.spec.ts` (2/2) — non-maximized round-trip + maximized round-trip (the regression). Reopen-closed-window 5/5, page-load-failure 5/5, unit 610/610. 85 - - 2026-04-27 Three polish fixes from user testing (round 2): (1) **Undo-close window inflated each cycle.** `main.ts` `closed` handler subtracted only fixed canvas chrome from OS window bounds when computing saveBounds — side-panel `extraWidth` (entities/notes/etc) stayed in the saved width. On reopen that became the new WEBVIEW size, panels re-expanded the window, and each cycle compounded ("wayyy wider"). Fixed by reading the page-host URL params (`x/y/width/height` reflect screenBounds — webview-only) at close time. Special case: when the page-host is in maximized mode, screenBounds equals the work area (window == webview, no canvas chrome); `updateUrlParams` now sets `maximized=1` and main.ts falls back to fixed-margin subtraction so reopen doesn't overshoot the display. Fall back to the same subtraction for paths that never set URL params. New regression test in `tests/desktop/reopen-closed-window.spec.ts` (URL-param round-trip via `__peek_test.getHostUrlParamsByUrl`). (2) **Lists tag rows broken card.** `getItemDisplayInfo` had no branch for `itemType === 'tag'` — tag results rendered with undefined title and the question-mark fallback icon; the trash-can `onDelete` would have called `datastore.deleteItem` against a tag id (wrong primitive). Added `tag` branch (title from `item.title`, subtitle `Tag (used N×)`, `#` icon) and suppressed `onDelete` for tag rows in `lists/home.js`. New unit tests in `card-helpers-favicon.test.js`. (3) **Session-restore parity test made deterministic.** Added `window:removed` pubsub event in `main.ts` (published after `windowRegistry.delete` — the authoritative post-cleanup signal, distinct from `window:closed` which fires earlier and would gate too soon). Test subscribes via Promise pattern. 86 - - 2026-04-27 `lists` bg+window consolidation: collapsed manifest to a single resident `home` tile, moved settings/command/shortcut wiring from `background.js` into `home.js`, deleted `background.{html,js}`. New `tests/desktop/lists-tile.spec.ts` 4/0 (manifest shape + keepLive, command registration, command-shows-window, keepLive cycle). `tags` deferred — see "Tile architecture cleanup" for rationale. 87 - - 2026-04-27 Tile architecture follow-up to lists/websearch consolidations (3 issues surfaced when user tested): (1) **Frame default flipped to false in tile-launcher.** `createTileBrowserWindow` previously defaulted `frame: hints?.frame !== false` (titlebar by default), divergent from the legacy `api.window.open` path that always defaulted to frameless. Resident tiles inherited a titlebar, which the user explicitly disallows. Now `frame: hints?.frame === true` — opt-in only, matching project policy "no OS titlebar anywhere, ever". (2) **`keepLive` wired into tile-launcher.** New optional `TileEntry.keepLive` field; when true, the BrowserWindow's `close` event is intercepted and the window is hidden instead of destroyed, so `showSelf()` from a command handler can re-reveal it instantly. Skipped during `app.before-quit` so shutdown isn't blocked. Added `keepLive: true` to lists + websearch manifests. (3) **Lists results spacing.** `.result-group` switched from `margin-bottom`-only to `display: flex; flex-direction: column; gap: 6px` so cards have breathing room. 88 - - 2026-04-27 Page-load failure error UI shipped: did-fail-load now renders a Peek-styled overlay with URL + reason + Retry/Close (`app/page/page.js` + `app/page/index.html`). Found that webview did-fail-load uses `validatedURL`, not `url`, and Chromium fires a second did-fail-load with empty URL for chrome-error pages — handler ignores those. Cleared on did-navigate to a non-`chrome-error://` URL. New `tests/desktop/page-load-failure.spec.ts` 4/0; redirect + navbar suites unaffected. 89 - - 2026-04-27 Cmd+shift+T (undo-close window) reliability fix: webview guest's `before-input-event` handler in `ipc.ts` had a hardcoded page-shortcut list (cmd+L/R/F/G/[/]) and didn't delegate unmatched shortcuts to `handleLocalShortcut`. Result: cmd+shift+T (and any top-level local shortcut) silently did nothing whenever focus was inside the webview — i.e. most of the time. Added an `else if (handleLocalShortcut(input, win.id))` fallback to both canvas and popup webview guest handlers. New `tests/desktop/reopen-closed-window.spec.ts` 4/0 (covers single round-trip, 5-iteration rapid loop, webview-content shortcut delivery, keepLive hide-vs-close stack invariant). Exposed `reopenLastClosedWindow`/`getClosedWindowStack`/`getClosedWindowCount` on `__peek_test` for tests. 90 - - 2026-04-27 Websearch bg+window consolidation finished: orphan `features/websearch/background.{html,js}` deleted (manifest already collapsed to a single `home` resident tile in earlier work; engine state + UI both live in `home.js`). All 10 websearch desktop tests green; no other refs in tree. Audit of remaining bg+window pubsub pairs added to "Tile architecture cleanup" — `tags` and `lists` are the next candidates. 91 - - 2026-04-26 Tag-action toggles fixed (6-phase plan): added `tag-actions:*` to tags/groups/search/pagestream pubsub allowlists; added proactive `tag-actions:get-all:response` broadcast in `features/tag-actions/home.js:init` to defeat consumer cold-start race; new `tests/desktop/tag-actions-toggles.spec.ts` (4/0). Search runtime round-trip is asserted statically (manifest topic check) due to an unrelated `search-home` workspace-key collapse blocking fresh test windows. 92 - - 2026-04-24 v1 removal complete: 36 commits stacked off main, every renderer routes through `tile-preload.cts` + strict `tile:*` IPC, manifestVersion 3 canonical, `extensions` SQLite table dropped, `extensionPaths` → `tilePaths`, `ext:*` startup topics → `feature:*`. Playwright 223/0, unit 2277/0. 93 - - 2026-04-23 Pubsub message-passing state machine landed — 8-phase conversion (`docs/pubsub-state-machine.md`). P4 added bgWindow-ready latch + private lifecycle IPC; P5 deleted `cmd:request-registers` replay machinery. Resolved root cause of "only hello-world commands visible in cmd panel". Unit 2284/0, Playwright 208/0/11-skipped. 94 - - 2026-04-23 Cmd-panel repeat invocation tests + 4 bugs fixed: panel state leak on keepLive reopen (private `tile:lifecycle:visible` IPC), `tile:window:close` now routes through `closeOrHideWindow`, per-invocation `cmd:execute:X:result` topic. 95 - - 2026-04-23 Consolidated command-result paths: `tile:command:result` side-channel deleted; all results flow through capability-gated `tile:pubsub:publish` of `cmd:execute:{name}:result:{uuid}`. 96 - - 2026-04-23 Theme live-reload in open tiles fixed (consolidated 3 broadcast channels into `tile:theme:changed`; preload-level cache-busted stylesheet injection). 97 - - 2026-04-23 Settings broken sections fixed: `api.profiles.*`, `api.adblocker.*`, `api.darkMode.*`, `api.sync.*` namespaces restored in tile-preload (lost in v1→v2 migration). Settings blank-on-restore fixed by stripping hash/query before special-case URL match. Pseudo-tile registry consolidates the 6 hard-coded peek:// URL checks. 98 - - 2026-04-23 Click-hold-drag window move restored (port `initWindowDrag()` IIFE from v1 preload into `tile-preload.cts`); page.js TDZ on `navbarGeneration` fixed. 99 - - 2026-04-23 `target=_blank` download popup cleanup fixed: blanket-register `will-download` via `app.on('web-contents-created')` so every webContents session is covered. 100 - - 2026-04-23 Bundled-extension links in page host widget verified working (was failing on missing `api.adblocker.getStatus()`). 101 - - 2026-04-23 Test infrastructure: moved cmd-panel-keyboard tests to `desktop-serial` (external-url, cmd-palette, edit-param-mode, cmd-chain). 229/0 stable across 5+ full runs. HUD disabled; tag-widget + maximize flakes resolved. 102 - - 2026-04-23 `yarn start` with `PROFILE=X` works alongside packaged Peek again (source builds always skip single-instance lock). 103 - - 2026-04-23 FSM-pilled Claude Code skill authored at `~/.claude/skills/fsm-pilled/SKILL.md`. 104 - - 2026-04-21 cmd / page / hud resident renderers consolidated into core background renderer (`app/index.js` → `initCmd/initPage/initHud`). `cmd-glue.ts`, `page-glue.ts`, `hud-glue.ts` deleted. 105 - - 2026-04-21 Fixed cmd `ext:ready` / extension-commands race; registry grows 72→96 commands on cold start. 106 - - 2026-04-21 Fixed Electron 40 console-message forwarder in `core-glue.ts` (level is string literal in E40, not int). 107 - - 2026-04-17 post.nl → postnl.nl redirect blank page fixed; download save failing fixed (resolveUniqueDownloadPath); single-file tiles default model shipped; bundled TWP translate extension; commands for browser-extension options pages. 108 - - 2026-04-16 Tiles v1-compat trimming complete (Phases 1-5): TILE_STRICT env flag, capability-violation pubsub topic, static tile-api.d.ts, all 26 features migrated to strict capabilities, v1 shims removed. 109 - - 2026-04-16 Atproto install hardening (CID validation, atomic install, minPeekVersion, icon/screenshot blob handling). 110 - - 2026-04-16 Pubsub audit follow-ups: tag lifecycle events, bare `tagItem()` bypasses converted to `tagItemAndPublish`. 111 - - 2026-04-16 Settings audit follow-ups: backend defaults, JSON Schema validation, isolation + defaults test coverage. 112 - - 2026-04-16 CSP gaps on error responses fixed (catch-block fallback applies CSP for tile origins). 113 - - 2026-04-15 Initial cmd extraction + tiles migration to v2 + features-manager phases 1-6. 10 + Migrated 2026-04-28: 16 active engineering todos (`[ ]`) moved into Peek as items with markdown bodies and `peek:*` machine tags. Plus ~245 existing `#peek` items had their bare `#todo` / `#desktop` / `#mobile` / `#daily-use` tags replaced with `peek:*` equivalents. The bare `#peek` project tag stays as the namespace marker, in advance of project workspaces.