Skip to content

refactor(multi-window): createWindow seams become marked insertions - #1811

Merged
donjor merged 1 commit into
hyprwsfrom
refactor/desktop-window-hooks
Oct 21, 2026
Merged

donjor merged 1 commit into
hyprwsfrom
refactor/desktop-window-hooks

Conversation

@donjor-agent

@donjor-agent donjor-agent Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🤖 claude/anthropic claude-opus-5-5 (high)

Problem

DesktopWindow.ts createWindow still carried 55 fork lines with no hook marker, so each sync that touched those upstream lines stopped for a manual resolution.

Change

Fork edits there become marked one-statement hooks; upstream's text returns verbatim around them.

  • Context menu: AppContextMenu.fork.ts
  • Demand state: WindowDemandState.fork.ts
  • Create target: requireWindowCreateTargetFork
  • Titles: retitleWindowFork
  • Renderer ready: its own hook
  • Hub-only preview session: one marked line

Unmarked residue drops from 201 to 76 lines.
The rest needs a behavior change (zoom, hub lookup, the ensureIdentity restructure) and stays flagged.

A follow-up fold moves each hook into the stack commit that owns it.

Scope and approval

Maintainer-authorized: the fold follow-up the maintainer asked for after the route-navigation reshape.

Verification

Check Result
desktop typecheck exit 0
vp test run apps/desktop/src/window/ 179 passed
vp run fork:ci exit 0

Written by Claude Opus 5.5 in Claude Code, hosted by T3 Code.

Fork trailers

Fork-Domain: multi-window
Fork-Tier: core

@rsi-software-agent

rsi-software-agent Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🧩 Effect Service Conventions 6e6daff ✅ clear

No findings.

🕐 21 Oct 10:25 UTC ⏱️ 1 min 🤖 gpt-6.1-sol 🪙 341.4K

⚠️ Not verified: Static review; checks not rerun. changedFiles lists PR-scoped head-to-head changes, excluding base-only updates.

Details
Head Result Time Check
6e6daff ✅ clear 1 min run
d350066 ✅ clear <1 min run
1e499e9 ✅ clear 1 min run

@rsi-software-agent

rsi-software-agent Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🩺 Aftercare 6e6daff ✅ clear

resolved 1

commit 1e499e9

  • A1 Keep shared-preview initialization out of project-window creation ✅ d350066

🕐 21 Oct 10:27 UTC ⏱️ 3 min 🤖 gpt-6.1-sol 🪙 986K

⚠️ Not verified: Continued session; live Electron/Hyprland placement and title sequencing unverified. Expanded changedFiles omitted: the…

Details

A1 Keep shared-preview initialization out of project-window creation · apps/desktop/src/window/DesktopWindow.ts:406

  • Problem: Project windows now fail when unrelated shared-preview session initialization fails.
  • Fix: Retain hub-only warmup and test project creation with failing shared-session initialization.
  • Note: Hub-only warmup restored; project-launch regression again asserts zero shared-session requests.
Head Result Time Check
6e6daff ✅ clear 3 min run
d350066 ✅ clear · 1 fixed 1 min run
1e499e9 🔴 1 open · 1 new 6 min run

@donjor-agent

donjor-agent Bot commented Oct 21, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

Requested a rerun for one exact failed or cancelled CI job on the unchanged pull-request head. GitHub may also rerun dependent jobs.

  • Head: 1e499e98435ba3669d37d8a7ff56f5a819d15672
  • Workflow: hyprws CI; run 39559263907; pre-rerun attempt 1; open run
  • Job: Test Web; id 119086324532; open job
  • Check run: 119086324532
  • Genuine flake reason: ComposerBannerStack web browser test timed out at 60s; PR touches only apps/desktop

@donjor
donjor force-pushed the refactor/desktop-window-hooks branch from 1e499e9 to d350066 Compare October 21, 2026 07:18
@donjor-agent

donjor-agent Bot commented Oct 21, 2026

Copy link
Copy Markdown
Author

🤖 claude/anthropic claude-opus-5-5 (high)

Requested a rerun for one exact failed or cancelled CI job on the unchanged pull-request head. GitHub may also rerun dependent jobs.

  • Head: d3500666158ab16e4a43dc99b8f2097bf7772ffc
  • Workflow: hyprws CI; run 39561018039; pre-rerun attempt 1; open run
  • Job: Test Web; id 119092543159; open job
  • Check run: 119092543159
  • Genuine flake reason: ComposerBannerStack browser test times out at 60s on CI only; passes locally 3/3; PR touches only apps/desktop; trunk Test Web also red

Fork edits inside DesktopWindow.ts createWindow, the context menu, and the
renderer-ready path become marked one-statement hooks, so a sync replays them
instead of conflicting on upstream lines.

Fork-Domain: multi-window
Fork-Tier: core
@donjor
donjor force-pushed the refactor/desktop-window-hooks branch from d350066 to 6e6daff Compare October 21, 2026 10:23
@donjor
donjor merged commit f1f2799 into hyprws Oct 21, 2026
@donjor
donjor deleted the refactor/desktop-window-hooks branch October 21, 2026 10:35
donjor added a commit that referenced this pull request Oct 21, 2026
…1811)

## Problem

`DesktopWindow.ts` `createWindow` still carried 55 fork lines with no hook marker, so each sync that touched those upstream lines stopped for a manual resolution.

## Change

Fork edits there become marked one-statement hooks; upstream's text returns verbatim around them.

- **Context menu:** `AppContextMenu.fork.ts`
- **Demand state:** `WindowDemandState.fork.ts`
- **Create target:** `requireWindowCreateTargetFork`
- **Titles:** `retitleWindowFork`
- **Renderer ready:** its own hook
- **Hub-only preview session:** one marked line

Unmarked residue drops from 201 to 76 lines.
The rest needs a behavior change (zoom, hub lookup, the `ensureIdentity` restructure) and stays flagged.

A follow-up fold moves each hook into the stack commit that owns it.

## Scope and approval

Maintainer-authorized: the fold follow-up the maintainer asked for after the route-navigation reshape.

## Verification

| Check | Result |
| --- | --- |
| desktop typecheck | exit 0 |
| `vp test run apps/desktop/src/window/` | 179 passed |
| `vp run fork:ci` | exit 0 |

Written by Claude Opus 5.5 in Claude Code, hosted by T3 Code.

## Fork trailers

Fork-Domain: multi-window
Fork-Tier: core
Co-authored-by: donjor <[email protected]>
donjor added a commit that referenced this pull request Oct 21, 2026
Squashes:

- b533812 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- b929e34 fixup! refactor(web): centralize thread route navigation
- 395c1f3 fixup! refactor(web): centralize thread route navigation
- 642f35f fixup! refactor(web): centralize thread route navigation
- d16eda2 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 21, 2026
Desktop app context menus now retain T3's theme, icons, and hierarchy instead of using native OS popups.

before: app action menus were rendered by Electron's native menu.

now: app action menus use the themed renderer, while spellcheck, media, and text-editing actions stay native.

![Themed Electron thread action menu](https://github.com/user-attachments/assets/07e7781e-a8c0-465a-9853-ce52dea23a4c)

## Mechanism

- App context menu requests stay in the renderer.
- The desktop host opens native menus only for Electron-exclusive actions.

Implemented with GPT-5.6 Codex in T3 Code.

Squashes:

- 3581900 fix(desktop): use themed app context menus (#1518) (#1572)
- f5adae0 fixup! fix(desktop): use themed app context menus
- 3d80224 fix(desktop): use themed app context menus (#1518) (#1572)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
donjor added a commit that referenced this pull request Oct 21, 2026
…kspaces (#224)

Squashes:

- e07e59b feat(desktop): launch Electron from T3 Code actions into Hyprland workspaces (#224) (#224) (#395) (#1573)
- ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689
- f6701d4 feat(desktop): launch Electron from T3 Code actions into Hyprland workspaces (#224) (#224) (#395) (#1573)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 21, 2026
Squashes:

- 7ee95f4 Suspend hidden managed zmux terminal attachments (#1574)
- fb75885 fixup! Suspend hidden managed zmux terminal attachments
- 4ef0e60 fixup! Suspend hidden managed zmux terminal attachments
- 8fb6b4e Suspend hidden managed zmux terminal attachments (#1574)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: zmux-estate
Fork-Tier: core
Fork-Upstreamable: no
donjor added a commit that referenced this pull request Oct 21, 2026
Squashes:

- c8a9569 feat(dev): open isolated test apps alongside stable (#1543)
- 7fd55b8 fix(desktop): an attached preview guest prepares in its own window's manager
- 865fe79 fix(desktop): desktop config and user-data fork tests compile against upstream's rework
- 5e3d870 feat(dev): open isolated test apps alongside stable (#1543)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: fork-meta
Fork-Tier: core
Fork-Upstreamable: no
donjor added a commit that referenced this pull request Oct 21, 2026
Squashes:

- 2a495c8 test(fork): move remaining pure fork titles behind the sibling boundary (#671) (#32)
- af2df9d fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1514) (#1514)
- 6ed88ba fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1553) (#1553)
- 1c2018e fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1589) (#1589)
- ab2ba58 test(fork): move remaining pure fork titles behind the sibling boundary (#671) (#32) (#1514) (#1553) (#1589)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: fork-meta
Fork-Tier: qol
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 21, 2026
…a second one

## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- 44e8f89 feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593)
- a560ece fixup! feat(desktop): attach to a running local backend instead of spawning a second one
- d0eef76 feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

- **Drain module:** startup restore and launch intents move to `DesktopStartupDrain.fork.ts`.
  - Bounded queue: capacity 8, drops newest with a log line, never blocks.
  - Renderer readiness and backend readiness share one drain.
  - The drain opens the restore first, then each intent in arrival order, once.
  - It re-polls until empty, so an intent queued mid-drain still opens.
  - One winner claims the drain; later callers await it.
  - A failed open is a typed `ElectronWindowCreateError`.
  - `markNotReady` re-arms it after a backend restart.
- **Second-instance buffer:** `DesktopSecondInstanceBuffer.fork.ts` listens from single-instance lock acquisition.
  - It holds launches that arrive before `DesktopClerk.configure` registers the real listener.
  - It replays them after the first launch's argv, in order.
- **Client-only snapshot:** client-only bootstrap now runs `snapShot.initialize`.

## Why

- **Client-only readiness:** client-only desktop never called `handleBackendReady`, so restore and launch intents only drained in managed mode.
  - An update relaunch dropped every window.
  - A `--project` cold start opened the hub.
- **Early second launch:** a second launch arriving within about 0.1s of the first was dropped.
  - Electron emitted `second-instance` before any app listener existed.

## Close conditions

- [x] Relaunch: client-only update restores windows. Live client-only Electron relaunch with a recorded `window-session.json` reopened exactly `zmux` and `solar`, with no hub. Fork test "opens the restore before an intent queued during it".
- [x] Cold start: client-only `--project` opens it. Live: `--project <solar>` cold start produced one page, the solar project route, with no hub.
- [x] Order: queued intents drain once, in order. Live: second-instance launches at 0.05s ×3, 0.1s ×3 and 0.4s ×2 each opened both windows, A then B, with no duplicate; before the buffer, 0.05–0.1s dropped B in 8 of 8 trials. Fork tests: straggler during an open, followers admitted mid-open, buffer replay once.
- [x] Bound: the queue is bounded. `STARTUP_INTENT_QUEUE_CAPACITY = 8`; the second-instance buffer shares the cap (test "holds at most the startup queue capacity").
- [x] Tests: both backend modes covered. Both readiness paths call the same drain; drain and buffer fork tests are mode-agnostic, and live proof ran in client-only mode.

Live proof ran at `ccde0e21a7`, off-screen on an agent workspace.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

No UI changes: startup window-open behavior only; no screenshots.

Closes #1340

## Fork trailers

Squashes:

- d9d39f7 fix(desktop): client-only boots drain restore and launch intent
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

Main mints a random `WindowId` (UUID v4, `node:crypto`) per desktop window and passes it to the renderer as the `--t3code-window-id` preload argument. The registry, preview ownership, Hyprland claims, the snapshot sender check and the update-restore manifest key by it. Project identity stays as data on the window record.

- `WindowRegistry.fork.ts` (new): registry keyed by `WindowId`; identity lookup is a scan; a restored id is reused unless a live window holds it.
- `WindowId.fork.ts` (new): brand, preload argument, validation; package-free for the sandboxed preload.
- `ElectronWindow.ts`, `DesktopWindow.ts`: one-line `multi-window` hooks delegate to the registry; `getOrCreate` passes the reserved id to `create`.
- `WindowPolicy*.ts`, `Manager.ts`: preview owner is `WindowId | "app"`; the bridge exposes `windowId`.
- `HyprlandPlacement.ts`: claims and moves key by `WindowId`.
- `DesktopWindowSession.ts`, `DesktopStartupDrain.fork.ts`, `DesktopUpdates.ts`: the manifest carries an optional `windowId`; restore reopens under the old id. A manifest from before this change still restores.

## Why

A project key cannot name two windows on the same project, or a window whose project changes. An opaque id keys the window itself, so #1342 through #1345 can build on it. None of them is pre-built here.

Closes #1341

### Close conditions

- [x] Registry: keyed by `WindowId`
  - `ElectronWindow.fork.test.ts`: "keys each window by a minted WindowId, never by what it shows", "reuses a restored WindowId unless a live window already holds it"
- [x] Previews: ownership keyed by `WindowId`
  - `WindowPolicy.fork.test.ts`, `preview.fork.test.ts`: owner routing by `WindowId`; `preview.test.ts` asserts owner `[windowId]`, not the project identity; `DesktopWindow.test.ts` asserts `setWindow` owner `[testWindowId(1)]`
- [x] Hyprland: claim keyed by `WindowId`
  - `DesktopWindow.test.ts`: claims and workspace moves carry `[hubId, projectWindowId]` on restore and `testWindowId(1)` on launch
  - Unit tests only: the live claim log is debug-level
- [x] Demand, snapshot: keyed by `WindowId`
  - Snapshot: `snapShotSender.fork.ts` resolves the sender through `windowIdFor`; `snapShot.fork.test.ts` covers a registered project window and an unregistered one
  - Demand: published per window on its own `webContents`; no project key exists on that path
- [x] Reload: id survives a renderer reload
  - Live (host, `.dump/evidence/1364-live/`): menu Reload (`22-ctrl-r.txt`), `forcefullyCrashRenderer` (`23-crash.txt`) and update restore (`40-*`, `41-renderer-ids.txt`) all keep the id
  - Unit: `DesktopWindow.fork.test.ts` pins the id across `render-process-gone` recovery; `WindowPolicy.fork.test.ts` reads the id from argv and leaves it unset when absent

### Swap proof (each reverted with `git checkout -- <file>`)

```diff
- registry keys by identity key         → ElectronWindow.fork.test 2 failed
- id dropped from preload arguments     → DesktopWindow.test 3 failed
- drain ignores entry.windowId          → DesktopWindow.test 1 failed
- manifest read drops windowId          → DesktopWindowSession.test 2 failed
- Hyprland claim keyed by identity      → DesktopWindow.test 1 failed
- id dropped (crash recovery test)      → DesktopWindow.fork.test 1 failed
- preview owner keyed by identity       → DesktopWindow.test 1 failed
```

### Checks

```
vp test run <targeted desktop dirs>   → exit 0 (64 files, 700 tests)
vp run fork:ci                        → exit 0 (scripts 59 files, 648 tests)
vpr typecheck                         → exit 0
vp run knip:check                     → exit 0
```

### Note

The crash-recovery case lives in `DesktopWindow.fork.test.ts`: the fork:ci upstream-test rule refuses a new case in `DesktopWindow.test.ts`. `registerMain` now reuses a registered window's id and never mints one the preload cannot see.

## UI Changes

None. Main-process window keying only.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes (n/a: no UI change)
- [x] I included a video for animation/interaction changes (n/a: no motion)

Model and harness: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- ef5a421 feat(desktop): key desktop windows by an opaque WindowId (#1576) (#1588) (#1590)
- ee337a4 fixup! feat(desktop): key desktop windows by an opaque WindowId
- 3ccc7bc fixup! feat(desktop): key desktop windows by an opaque WindowId
- 69a9cc5 fixup! feat(desktop): key desktop windows by an opaque WindowId
- cd0487c feat(desktop): key desktop windows by an opaque WindowId (#1576) (#1588) (#1590)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 21, 2026
…indow

## What Changed

Every "main" window lookup in main now resolves by one rule.

| Request | Target |
| --- | --- |
| From a renderer (IPC) | The sender window |
| App-wide (menu, dock, `t3 app`, capture) | Most recently focused app window |

Only windows that main registered can be targets, so the splash, DevTools, and popups never are.
The upstream fallback runs only while no window is registered yet.

| File | Change |
| --- | --- |
| `WindowTargets.fork.ts` (new) | `IpcRequester` reference; `DesktopIpc` provides the sender around every invoke and sync handler |
| `WindowRegistry.fork.ts` | Focus recency per `WindowId`, dropped on close; `requestTarget` is the sender if registered, else the most recent window; `windowsByRecency` lists windows newest first |
| `ElectronWindow.ts` | `main`, `currentMainOrFirst`, `focusedMainOrFirst` return `requestTarget` first |
| `DesktopWindow.ts` | `activate` and `prepareCaptureReveal` use the capture's pinned window while a capture is acquiring, else the target; hub creation stays hub-only |
| `SnapShotTarget.fork.ts` (new) | A capture pins its window on `requested`; later events, the reveal, the post-capture activation, and the Linux destination window follow the pin. The pin is released on `ready`, `failed`, or when the capture fiber ends, interruption included |
| `DesktopAppActivationRenderers.fork.ts` (new) | Readiness per sender window; a request walks the ready windows in focus-recency order; an unregistered sender is ignored and never clears readiness; losing a window fails only the request it was handling |

### Audit

| Consumer | Resolves to |
| --- | --- |
| `ElectronWindow.main`, `currentMainOrFirst`, `focusedMainOrFirst` | Sender, else most recent (hooked once here) |
| `getWindowFullscreenState` (sync IPC) | Sender |
| `pickFolder`, `pickProjectFavicon`, `pickThemeFiles` owner | Sender |
| `showContextMenu` | Sender |
| `pasteAsText` main check | Sender: project windows can now paste as text |
| `ensureTrustedSnapShotSender` | Sender; the registered-window fallback is now redundant but kept |
| `DesktopSshPasswordPrompts.request` | Requester inside IPC, else most recent |
| `DesktopAppActivation.setRendererReady` | Per registered sender; an unregistered sender is ignored |
| `DesktopWindow.activate` (dock, `t3 app`) | Most recent |
| `DesktopWindow.activate` (post-capture) | Window pinned at `requested` |
| `prepareCaptureReveal` | Window pinned at `requested` |
| `dispatchMenuAction`, `zoomMain` | Most recent |
| `dispatchSnapShotEvent` | Window pinned at `requested` |
| `DesktopSnapShot` `destinationWindow` | Window pinned at `requested` |
| `openSystemSettings`, snapshot permission and setup owners | Kept: on macOS the focused window is the sender |
| `notificationBadge` | Kept: app-wide badge |
| `sendAll`, `destroyAll`, `syncAllAppearance` | Kept: broadcast |
| Preview `setMainWindow` owner | Kept: `"app"` owner from #1341 |
| `mainWindowBounds`, `flushMainWindowBounds` | Kept: hub bounds, #1345 |
| `ensureMain`, `createMain`, `revealOrCreateMain`, `createMainIfBackendReady`, `showConnectingSplash` | Kept: hub creation, #1343 |

## Why

Upstream binds IPC answers, dialogs, captures, and activation to the single hub window.

| Flow | Before, with project windows |
| --- | --- |
| Dialog from a project window | Parented to the hub |
| Fullscreen state | Read from the hub |
| One window's cleanup | Cleared `t3 app` activation for all |

Closes #1342

### Close conditions

- [x] Audit: every `main`-style consumer resolved
- [x] Fullscreen: targets the sender
- [x] Async: dialog, capture return to requester
- [x] Activation: readiness is per sender
- [x] Close one: others keep activation
- [x] Exclusions: no splash, DevTools, or popup

| Condition | Evidence in `apps/desktop/src/electron/WindowTargets.fork.test.ts` |
| --- | --- |
| Audit | Audit table above; "an IPC request resolves every main-style lookup to the sending window" |
| Fullscreen | "fullscreen state is read from the sender, not the hub", through `DesktopIpc.handleSync` |
| Async | "a dialog opened from a window is owned by that window while another has focus"; "every event of one capture reaches the window its request pinned"; "the capture reveals and activates the window it pinned"; "an interrupted capture releases its pinned window" |
| Activation | "a request goes to the most recently focused ready window"; "with the focused window not ready, a request walks focus recency"; "a renderer main did not register cannot clear the app windows" |
| Close one | "closing one ready window leaves the others activated"; "losing the window handling a request fails only that request" |
| Exclusions | "an app-wide request resolves to the most recently focused app window only": a focused unregistered window and an unregistered sender both resolve to the last app window |

### Live checks (host, not run here)

| Check | Steps and expectation |
| --- | --- |
| Fullscreen | Fullscreen a project window and reload it; its chrome reads fullscreen and the hub's does not |
| Dialog | Focus the hub, then pick a folder from a project window; the sheet parents to the project window |
| Capture | Snap shot with a project window last focused; the animation and result land there |
| Activation | Open the hub and a project, close the project, run `t3 app`; the request still completes |
| Exclusions | Focus detached DevTools, then use a menu action; it reaches the last app window |

### Swap proof

Temporary reverts at `f38334a280`, not committed:

| Reverted | `WindowTargets.fork.test.ts` |
| --- | --- |
| `ElectronWindow.ts` and `DesktopIpc.ts` hooks | 6 of 8 fail |
| `setRendererReady` hook | 3 of 8 fail (every activation case) |

### Checks

```text
vp test run <12 desktop files>  → exit 0 (12 files, 156 tests)
vp run fork:ci                  → exit 0 (scripts: 59 files, 648 tests)
vpr typecheck                   → exit 0
vp run knip:check               → exit 0
```

**Note:** `activate` now reveals the most recent window, not the hub, which also affects the dock behaviour in #1343.

## UI Changes

None: this changes which window main targets and adds no UI.

## Checklist

- [x] Small and focused
- [x] Explains what and why
- [x] Screenshots: n/a, no UI change
- [x] Video: n/a, no motion

Model and harness: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- c3c9d16 feat(desktop): window requests target the sender or the most recent window
- 14073c5 fixup! feat(desktop): window requests target the sender or the most recent window
- c74e5dc feat(desktop): window requests target the sender or the most recent window
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

Every window request resolves through one table in `apps/desktop/src/window/WindowDispatch.fork.ts`.
The startup drain and the second-instance buffer use the same table.

| Request | Behavior |
| --- | --- |
| New Window (menu, palette, key) | create at `/`, all projects |
| Open in New Window (row, thread) | create at that route, seed filter |
| Second launch, dock | focus most recent, else create |
| Deep link, `--project` | reuse a window showing it, else create |
| Focus window by id | focus if open; never resurrect |
| OAuth callback | unchanged |

- **Surfaces:** File > New Window, palette action, thread menu item
- **Bindings:** `window.new` on `mod+shift+w`
- **Rename:** `project.openWindow` becomes `window.openInNew`
- **Migration:** saved bindings with the old id load unchanged
- **IPC:** `openProjectWindow` replaced by `requestWindow`
- **Seed:** New Window seeds `"all-projects"`; Open in New Window and deep links seed their project
- **Recency:** a second launch focuses the first of `windowsByRecency`
- **Scope guard:** kept; a project window that leaves its project still closes, and closes itself rather than the first window sharing its project
- **List routes:** Issues and Pull requests keep a project window in its project from a shared page
- **Other project's item:** an issue or pull request link from another project widens the list to all projects, reading the window's project when the pathname names none
- **Guard close:** skipped when the window is already destroyed by the time the hub is revealed
- **Queued second launch:** drained after boot, it only creates main when none exists and never reveals a window

## Why

Closes #1343

Closes #1344

Window requests had separate paths with different reuse rules.
One table makes each row testable and keys windows by `WindowId` only.

### Issue work route

Issue work from `/project/<env>/<project>/issues` drafts at `/project/<env>/<project>/draft/<id>`, which the guard allows.
The route that bounced was the hub `/issues`.
Issues, from a shared page such as `/settings` in a project window, read the project from the pathname, found none, and navigated to `/issues`.
The guard then revealed the hub and closed the window.
`listRouteTarget` now falls back to the window's own project, so Issues and Pull requests stay on the project routes.
#179 left the route unconfirmed, and no desktop log still holds a `project window left its scope` line.
The trace came from the source; live proof then confirmed the fixed route (steps 70–74 below).

### Known limits

Refs #1345

- **Update relaunch:** extra hub or same-project windows are dropped; `DesktopWindowSession` dedups by identity and the startup drain restores through `getOrCreate`
- **New Window as hub:** when main closes, a New Window becomes main, but its bounds are never saved

## Close conditions

- [x] Table: each row has a focused test
  - `WindowDispatch.fork.test.ts`: one test per row, plus argv classification
- [x] Binding: `mod+shift+w`, conflict-checked
  - `keybindings.fork.test.ts`: no other default owns the chord
- [x] `mod+alt+o`: renamed; now always opens a new window, per the table
  - live: a second press creates a second window where `openIdentity` reused one (step 41)
  - migration of existing `keybindings.json`: unit-tested, not live
- [x] Issue work: thread opens in the source window
  - live: steps 70–74; issues from settings stay in the project, the draft opens there, and the guard closes only itself
  - `DesktopWindow.fork.test.ts`: issue work stays in its window; another project is still guarded
  - `projectRoutes.test.ts`: lists from a shared page stay in the window's project
- [x] Seed (#1344): New Window opens on all projects; Open in New Window opens on its project
  - live: New Window by key, menu, and palette (steps 20–22); Open in New Window by row and thread menu (steps 30–31)
  - `DesktopWindow.fork.test.ts`: the seed preload argument per request
- [x] Deep link: reuses a window showing the project, else creates one
  - live: steps 50–51
- [x] Second launch: focuses the most recent window
  - live: steps 63 and 65; Linux has no dock activate

Live proof ran at `025eb14c81` on a client-only dev Electron over a seeded copy, 6/6 pass, no stray windows.
Evidence: `.dump/evidence/1369-live/` on the host, indexed by `transcript.json`; step N is its `N-*` files.

## Checks

```text
vp test run (desktop window/ipc/app/electron) → exit 0, 60 files, 409 tests
vp test run (web links, routes, sidebar)      → exit 0, 17 files, 539 tests
vpr typecheck                            → exit 0
vp run knip:check                        → exit 0
vp run fork:ci                           → exit 0
```

Swap proofs, each reverted:

```diff
- open-in-new-window reuses a showing window  → 1 test fails
- activate goes to openPrimary                → 1 test fails
- migration returns input unchanged           → 2 tests fail
- New Window default mod+shift+n              → 2 tests fail
- dispatched window counted as primary        → 1 test fails
- guard never closes                          → 2 tests fail
- guard allows only the exact project route   → 1 test fails
- list route ignores the window's project     → 1 test fails
- seed not passed to the preload              → 1 test fails
- most recent read from the oldest window     → 1 test fails
- guard closes a destroyed window             → 1 test fails
- drained second launch takes the table row   → 1 test fails
- link scope ignores the window's project     → 1 test fails
```

## UI Changes

New menu, palette, and thread-menu items.
Screenshots stay in the host evidence directory above; they show private thread titles.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Model: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- 9d6e5df feat(desktop): New Window and deep links follow one dispatch table
- fd56cc1 fix(desktop): mod+alt+o reuses the project's window again
- 7d18eaf feat(desktop): New Window and deep links follow one dispatch table
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

| Area | Change |
| --- | --- |
| Manifest | v2: one row per window, `{ windowId, route, bounds?, workspace? }` |
| Restore | keyed by `windowId`; extra windows open via `createNew` |
| Bounds | the current main window saves them, and hands over on close |
| Token | only a window restored onto a workspace maps as `t3code-window-<WindowId>` |
| Rules | every rule staged on a token is removed when placement ends |
| Claims | exact title, else the one new client since show; atomic select and record |

## Why

Update relaunch dropped extra hub and same-project windows.
A New Window that became main never saved its bounds.

## Close conditions

### Manifest

- [x] **Route:** an invalid route falls back to `/`
- [x] **v1 project entry:** becomes a seeded window
- [x] **v1 hub entry:** becomes an all-projects window
- [x] **Bounds:** display-validated
- [x] **When:** update relaunch only
- [x] **Corrupt manifest:** still opens one window

### Placement

- [x] **Token:** the title carries the WindowId until claimed
- [x] **Claims:** serialized, never guessed
- [ ] **Live:** real Hyprland update relaunch

Placement boxes are proven against a faked compositor; the host arranges live proof.

### Notes

| Note | Detail |
| --- | --- |
| `project` field | required: a route alone cannot tell a project window from an all-projects one |
| Token scope | other windows keep their normal title, so user rules on it still fire |
| Rule cleanup | Lua: workspace and suppression rules disabled, handle globals cleared |
| Rule names | one compositor-wide counter; no name is ever reused |
| Legacy grammar | no unset for `suppress_event`, so tokens stage workspace only |
| No claim lock | select and record are atomic by construction |
| Known limit | all-projects windows without bounds open stacked at main's bounds |
| Known limit | two normal windows mapping at once, both retitled, stay unclaimed |

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes (none)
- [ ] I included a video for animation/interaction changes (none)

Closes #1345

Written by Claude Opus 5.5 in Claude Code, hosted by T3 Code.

## Fork trailers

Squashes:

- adf1305 feat(desktop): update relaunch restores every window from manifest v2 (#1577)
- d40e3ab fixup! feat(desktop): update relaunch restores every window from manifest v2
- 4d5a32f feat(desktop): update relaunch restores every window from manifest v2 (#1577)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

| Area | Change |
| --- | --- |
| Web routes | `project.$environmentId.$projectId*`, `projectRoutes.ts` removed |
| Web navigation | `threadRouteNavigation.ts` removed |
| Web scope | window scope, toggle, sidebar lock, `forcedProjectRef`, `scope=all` removed |
| Web chooser | multi-select chooser enabled in every window |
| Web landing | a window's first draft starts in its filter or seed |
| Web lists | a linked project outside the filter is still read |
| Web decoder | `/project/$` maps old links to ordinary routes |
| Web drafts | `:project:` buckets fold into route-free buckets |
| Desktop | scope guard, `projectWindowArgument.ts`, preload `projectWindowRef` removed |
| Desktop windows | project-link windows open at `/` with their seed |
| Scripts | hook scan accepts restored upstream comments and same-file export moves |
| Mobile | no project-route producers; unchanged |

### Decoder forms

| Old link | Lands on |
| --- | --- |
| `/project/E/P` | `/` |
| `/project/E/P/thread/T` | `/E/T` |
| `/project/E/P/draft/D` | `/draft/D` |
| `/project/E/P/issues` | `/issues` filtered to `P` |
| `/project/E/P/pull-requests` | `/pull-requests` filtered to `P` |
| `…?scope=all` | list without a project filter |
| Hash, slash-hash, escaped ids | same as above |
| Anything else | `/` |

## Why

A project window is a filter, not a route scope.
Issues and PR "All projects" now means the window's project filter.

## Close conditions

- [x] **Producers:** no code produces a project route
- [x] **Decoder:** old links reach ordinary routes
- [x] **Issues/PR:** "all" means all in this window
- [x] **Fold:** no upstream line deleted then restored

## Refs #1336

- **Drafts:** migrated on start, no text lost
- **Delta:** `project-windows` stays
- **Delta blockers:** live markers, history trailers

## Follow-ups

- **Out-of-filter links:** may not resolve on the list
- **Scan fix:** fork-meta tooling bundled here

Closes #1347

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Opus 5.5 via Claude Code; no live UI proof in scope.

## Fork trailers

Squashes:

- a132d4e refactor(web): project routes retire behind a link decoder (#1578)
- 95d92d3 fixup! refactor(web): project routes retire behind a link decoder
- b0f1f68 refactor(web): project routes retire behind a link decoder (#1578)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

The window's project filter now scopes lists and never hides running work.

| Area | Change |
| --- | --- |
| Outside thread | Header **Add project** adds its entry |
| Palette | Threads and projects scoped |
| Search all | Palette toggle reaches every project |
| New thread | Several projects: opens the scoped picker |
| Palette hint | `chat.new` hint only where it lands the same |
| Title | Web and hub desktop titles append the label |

**New thread rules:**

- **All:** upstream behavior
- **One project:** that project, viewed member first
- **Several:** picker; never a silent pick
- **Direct:** viewed project (shift+click, `chat.newLocal`)

**Gate:** the sidebar's chooser host.
The legacy sidebar and routes without the sidebar keep upstream behavior.

**Landing:** the index draft (#1347) keeps its most-recent pick inside the filter.
It is a landing, not a new-thread request, so it never asks.

## Why

A filtered window must narrow what it lists without closing threads or stopping work in other projects.
The filter is read only by lists, the chooser, the palette, and the header action.
Turns, previews, and terminals are keyed by thread, so filter changes never reach them.

### Surfaces

| Surface | Decision |
| --- | --- |
| Sidebar new thread | Filtered |
| Palette | Scoped, **Search all** toggle |
| `chat.new`, `chat.newLocal` | Filtered |
| Desktop title | Hub adds label; `t3code-window-` claim title held |
| Palette "New thread in" | Hint dropped when `chat.new` differs |
| Mobile | N/A: single-select, no palette or window title |
| Contracts | No wire change |
| Providers | N/A |

**Known limit:** Settings has no sidebar, so the title drops the label and the palette is unscoped there.

## UI Changes

Live Electron proof at `e6b38d7cc4`, before the rebase onto #1377:

![04-outside-thread-open-add-project.png](https://github.com/user-attachments/assets/3b586553-08d7-4555-929c-455b2d407e27)

![05-after-add-project.png](https://github.com/user-attachments/assets/fe9e2228-63ce-472d-ba18-c8c38feba934)

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

## Close conditions

- [x] **Outside:** opening one never closes it
- [x] **Add project:** adds its entry
- [x] **Search all:** reaches every project
- [x] **New thread:** never picks a member silently
- [x] **Title:** shows the filter label

Outside and Title: live proof at `e6b38d7cc4`; the rebase keeps both hooks.

Closes #1353

Model: Claude Opus 5.5 in Claude Code (T3 Code host).

## Fork trailers

Squashes:

- 6987e82 feat(web): the window filter scopes lists without hiding running work
- 99aeccb fixup! feat(web): the window filter scopes lists without hiding running work
- 626e141 fixup! feat(web): the window filter scopes lists without hiding running work
- 733723b feat(web): the window filter scopes lists without hiding running work
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: workspaces
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
## What Changed

- `WindowPlacement.placeAtMapFork` takes `maximize` and calls `window.maximize()` after `showInactive()`, so the window is already mapped under its staged workspace rule.
- `DesktopWindow.ts`: the placement branch runs before the upstream `mainWindowMaximized` maximize. That maximize and the focused reveal now run only on the path with no placement, which is unchanged.
- New test: a placed maximized window records `stage-rule`, `show-inactive`, `maximize` in that order, with no `show` and no `focus`.

## Why

Electron's `maximize()` shows a hidden window. The reveal trigger called it before the placement staged the Hyprland rule, so the window mapped on the focused workspace, then moved. On Hyprland `mainWindowMaximized` is normally `true`. A control run with it `false` mapped correctly and did not move.

Dev agent placement and restored windows both use `placeAtMapFork`, so this fix covers both. On a window that is already visible, Chromium's maximize only requests the maximized state and does not activate the window. The packaged re-run still has to prove that Hyprland keeps focus where it is.

Closes #261

- [ ] A project window maps on its intended workspace at first map, with no later move
- [ ] Neither monitor changes its active workspace when a project window opens

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Claude Code, claude-opus-5-5.

## Fork trailers

Squashes:

- c756f91 fix(desktop): a placed window maps on its workspace before it maximizes
- 3d3cb5a fix(desktop): windows opened together each claim their own Hyprland client
- 8f8346c fix(desktop): a placed window maps on its workspace before it maximizes
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 21, 2026
…1811)

The part of the createWindow reshape that stays in fork-owned files; its DesktopWindow.ts hooks folded into their owners.

Squashes:

- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor added a commit that referenced this pull request Oct 21, 2026
Squashes:

- b533812 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- b929e34 fixup! refactor(web): centralize thread route navigation
- 395c1f3 fixup! refactor(web): centralize thread route navigation
- 642f35f fixup! refactor(web): centralize thread route navigation
- d16eda2 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 21, 2026
…1811)

The part of the createWindow reshape that stays in fork-owned files; its DesktopWindow.ts hooks folded into their owners.

Squashes:

- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor added a commit that referenced this pull request Oct 22, 2026
Squashes:

- b533812 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- b929e34 fixup! refactor(web): centralize thread route navigation
- 395c1f3 fixup! refactor(web): centralize thread route navigation
- 642f35f fixup! refactor(web): centralize thread route navigation
- d16eda2 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
Desktop app context menus now retain T3's theme, icons, and hierarchy instead of using native OS popups.

before: app action menus were rendered by Electron's native menu.

now: app action menus use the themed renderer, while spellcheck, media, and text-editing actions stay native.

![Themed Electron thread action menu](https://github.com/user-attachments/assets/07e7781e-a8c0-465a-9853-ce52dea23a4c)

## Mechanism

- App context menu requests stay in the renderer.
- The desktop host opens native menus only for Electron-exclusive actions.

Implemented with GPT-5.6 Codex in T3 Code.

Squashes:

- 3581900 fix(desktop): use themed app context menus (#1518) (#1572)
- f5adae0 fixup! fix(desktop): use themed app context menus
- 3d80224 fix(desktop): use themed app context menus (#1518) (#1572)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
donjor added a commit that referenced this pull request Oct 22, 2026
…kspaces (#224)

Squashes:

- e07e59b feat(desktop): launch Electron from T3 Code actions into Hyprland workspaces (#224) (#224) (#395) (#1573)
- ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689
- f6701d4 feat(desktop): launch Electron from T3 Code actions into Hyprland workspaces (#224) (#224) (#395) (#1573)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
Squashes:

- 6aba10e Suspend hidden managed zmux terminal attachments (#1574) (#1811)
- 2023697 fixup! Suspend hidden managed zmux terminal attachments

Fork-Domain: zmux-estate
Fork-Tier: core
Fork-Upstreamable: no
donjor added a commit that referenced this pull request Oct 22, 2026
Squashes:

- c8a9569 feat(dev): open isolated test apps alongside stable (#1543)
- 7fd55b8 fix(desktop): an attached preview guest prepares in its own window's manager
- 865fe79 fix(desktop): desktop config and user-data fork tests compile against upstream's rework
- 5e3d870 feat(dev): open isolated test apps alongside stable (#1543)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: fork-meta
Fork-Tier: core
Fork-Upstreamable: no
donjor added a commit that referenced this pull request Oct 22, 2026
Squashes:

- 2a495c8 test(fork): move remaining pure fork titles behind the sibling boundary (#671) (#32)
- af2df9d fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1514) (#1514)
- 6ed88ba fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1553) (#1553)
- 1c2018e fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1589) (#1589)
- ab2ba58 test(fork): move remaining pure fork titles behind the sibling boundary (#671) (#32) (#1514) (#1553) (#1589)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: fork-meta
Fork-Tier: qol
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
…a second one

## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- 44e8f89 feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593)
- a560ece fixup! feat(desktop): attach to a running local backend instead of spawning a second one
- d0eef76 feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

- **Drain module:** startup restore and launch intents move to `DesktopStartupDrain.fork.ts`.
  - Bounded queue: capacity 8, drops newest with a log line, never blocks.
  - Renderer readiness and backend readiness share one drain.
  - The drain opens the restore first, then each intent in arrival order, once.
  - It re-polls until empty, so an intent queued mid-drain still opens.
  - One winner claims the drain; later callers await it.
  - A failed open is a typed `ElectronWindowCreateError`.
  - `markNotReady` re-arms it after a backend restart.
- **Second-instance buffer:** `DesktopSecondInstanceBuffer.fork.ts` listens from single-instance lock acquisition.
  - It holds launches that arrive before `DesktopClerk.configure` registers the real listener.
  - It replays them after the first launch's argv, in order.
- **Client-only snapshot:** client-only bootstrap now runs `snapShot.initialize`.

## Why

- **Client-only readiness:** client-only desktop never called `handleBackendReady`, so restore and launch intents only drained in managed mode.
  - An update relaunch dropped every window.
  - A `--project` cold start opened the hub.
- **Early second launch:** a second launch arriving within about 0.1s of the first was dropped.
  - Electron emitted `second-instance` before any app listener existed.

## Close conditions

- [x] Relaunch: client-only update restores windows. Live client-only Electron relaunch with a recorded `window-session.json` reopened exactly `zmux` and `solar`, with no hub. Fork test "opens the restore before an intent queued during it".
- [x] Cold start: client-only `--project` opens it. Live: `--project <solar>` cold start produced one page, the solar project route, with no hub.
- [x] Order: queued intents drain once, in order. Live: second-instance launches at 0.05s ×3, 0.1s ×3 and 0.4s ×2 each opened both windows, A then B, with no duplicate; before the buffer, 0.05–0.1s dropped B in 8 of 8 trials. Fork tests: straggler during an open, followers admitted mid-open, buffer replay once.
- [x] Bound: the queue is bounded. `STARTUP_INTENT_QUEUE_CAPACITY = 8`; the second-instance buffer shares the cap (test "holds at most the startup queue capacity").
- [x] Tests: both backend modes covered. Both readiness paths call the same drain; drain and buffer fork tests are mode-agnostic, and live proof ran in client-only mode.

Live proof ran at `ccde0e21a7`, off-screen on an agent workspace.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

No UI changes: startup window-open behavior only; no screenshots.

Closes #1340

## Fork trailers

Squashes:

- d9d39f7 fix(desktop): client-only boots drain restore and launch intent
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

Main mints a random `WindowId` (UUID v4, `node:crypto`) per desktop window and passes it to the renderer as the `--t3code-window-id` preload argument. The registry, preview ownership, Hyprland claims, the snapshot sender check and the update-restore manifest key by it. Project identity stays as data on the window record.

- `WindowRegistry.fork.ts` (new): registry keyed by `WindowId`; identity lookup is a scan; a restored id is reused unless a live window holds it.
- `WindowId.fork.ts` (new): brand, preload argument, validation; package-free for the sandboxed preload.
- `ElectronWindow.ts`, `DesktopWindow.ts`: one-line `multi-window` hooks delegate to the registry; `getOrCreate` passes the reserved id to `create`.
- `WindowPolicy*.ts`, `Manager.ts`: preview owner is `WindowId | "app"`; the bridge exposes `windowId`.
- `HyprlandPlacement.ts`: claims and moves key by `WindowId`.
- `DesktopWindowSession.ts`, `DesktopStartupDrain.fork.ts`, `DesktopUpdates.ts`: the manifest carries an optional `windowId`; restore reopens under the old id. A manifest from before this change still restores.

## Why

A project key cannot name two windows on the same project, or a window whose project changes. An opaque id keys the window itself, so #1342 through #1345 can build on it. None of them is pre-built here.

Closes #1341

### Close conditions

- [x] Registry: keyed by `WindowId`
  - `ElectronWindow.fork.test.ts`: "keys each window by a minted WindowId, never by what it shows", "reuses a restored WindowId unless a live window already holds it"
- [x] Previews: ownership keyed by `WindowId`
  - `WindowPolicy.fork.test.ts`, `preview.fork.test.ts`: owner routing by `WindowId`; `preview.test.ts` asserts owner `[windowId]`, not the project identity; `DesktopWindow.test.ts` asserts `setWindow` owner `[testWindowId(1)]`
- [x] Hyprland: claim keyed by `WindowId`
  - `DesktopWindow.test.ts`: claims and workspace moves carry `[hubId, projectWindowId]` on restore and `testWindowId(1)` on launch
  - Unit tests only: the live claim log is debug-level
- [x] Demand, snapshot: keyed by `WindowId`
  - Snapshot: `snapShotSender.fork.ts` resolves the sender through `windowIdFor`; `snapShot.fork.test.ts` covers a registered project window and an unregistered one
  - Demand: published per window on its own `webContents`; no project key exists on that path
- [x] Reload: id survives a renderer reload
  - Live (host, `.dump/evidence/1364-live/`): menu Reload (`22-ctrl-r.txt`), `forcefullyCrashRenderer` (`23-crash.txt`) and update restore (`40-*`, `41-renderer-ids.txt`) all keep the id
  - Unit: `DesktopWindow.fork.test.ts` pins the id across `render-process-gone` recovery; `WindowPolicy.fork.test.ts` reads the id from argv and leaves it unset when absent

### Swap proof (each reverted with `git checkout -- <file>`)

```diff
- registry keys by identity key         → ElectronWindow.fork.test 2 failed
- id dropped from preload arguments     → DesktopWindow.test 3 failed
- drain ignores entry.windowId          → DesktopWindow.test 1 failed
- manifest read drops windowId          → DesktopWindowSession.test 2 failed
- Hyprland claim keyed by identity      → DesktopWindow.test 1 failed
- id dropped (crash recovery test)      → DesktopWindow.fork.test 1 failed
- preview owner keyed by identity       → DesktopWindow.test 1 failed
```

### Checks

```
vp test run <targeted desktop dirs>   → exit 0 (64 files, 700 tests)
vp run fork:ci                        → exit 0 (scripts 59 files, 648 tests)
vpr typecheck                         → exit 0
vp run knip:check                     → exit 0
```

### Note

The crash-recovery case lives in `DesktopWindow.fork.test.ts`: the fork:ci upstream-test rule refuses a new case in `DesktopWindow.test.ts`. `registerMain` now reuses a registered window's id and never mints one the preload cannot see.

## UI Changes

None. Main-process window keying only.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes (n/a: no UI change)
- [x] I included a video for animation/interaction changes (n/a: no motion)

Model and harness: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- ef5a421 feat(desktop): key desktop windows by an opaque WindowId (#1576) (#1588) (#1590)
- ee337a4 fixup! feat(desktop): key desktop windows by an opaque WindowId
- 3ccc7bc fixup! feat(desktop): key desktop windows by an opaque WindowId
- 69a9cc5 fixup! feat(desktop): key desktop windows by an opaque WindowId
- cd0487c feat(desktop): key desktop windows by an opaque WindowId (#1576) (#1588) (#1590)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
…indow

## What Changed

Every "main" window lookup in main now resolves by one rule.

| Request | Target |
| --- | --- |
| From a renderer (IPC) | The sender window |
| App-wide (menu, dock, `t3 app`, capture) | Most recently focused app window |

Only windows that main registered can be targets, so the splash, DevTools, and popups never are.
The upstream fallback runs only while no window is registered yet.

| File | Change |
| --- | --- |
| `WindowTargets.fork.ts` (new) | `IpcRequester` reference; `DesktopIpc` provides the sender around every invoke and sync handler |
| `WindowRegistry.fork.ts` | Focus recency per `WindowId`, dropped on close; `requestTarget` is the sender if registered, else the most recent window; `windowsByRecency` lists windows newest first |
| `ElectronWindow.ts` | `main`, `currentMainOrFirst`, `focusedMainOrFirst` return `requestTarget` first |
| `DesktopWindow.ts` | `activate` and `prepareCaptureReveal` use the capture's pinned window while a capture is acquiring, else the target; hub creation stays hub-only |
| `SnapShotTarget.fork.ts` (new) | A capture pins its window on `requested`; later events, the reveal, the post-capture activation, and the Linux destination window follow the pin. The pin is released on `ready`, `failed`, or when the capture fiber ends, interruption included |
| `DesktopAppActivationRenderers.fork.ts` (new) | Readiness per sender window; a request walks the ready windows in focus-recency order; an unregistered sender is ignored and never clears readiness; losing a window fails only the request it was handling |

### Audit

| Consumer | Resolves to |
| --- | --- |
| `ElectronWindow.main`, `currentMainOrFirst`, `focusedMainOrFirst` | Sender, else most recent (hooked once here) |
| `getWindowFullscreenState` (sync IPC) | Sender |
| `pickFolder`, `pickProjectFavicon`, `pickThemeFiles` owner | Sender |
| `showContextMenu` | Sender |
| `pasteAsText` main check | Sender: project windows can now paste as text |
| `ensureTrustedSnapShotSender` | Sender; the registered-window fallback is now redundant but kept |
| `DesktopSshPasswordPrompts.request` | Requester inside IPC, else most recent |
| `DesktopAppActivation.setRendererReady` | Per registered sender; an unregistered sender is ignored |
| `DesktopWindow.activate` (dock, `t3 app`) | Most recent |
| `DesktopWindow.activate` (post-capture) | Window pinned at `requested` |
| `prepareCaptureReveal` | Window pinned at `requested` |
| `dispatchMenuAction`, `zoomMain` | Most recent |
| `dispatchSnapShotEvent` | Window pinned at `requested` |
| `DesktopSnapShot` `destinationWindow` | Window pinned at `requested` |
| `openSystemSettings`, snapshot permission and setup owners | Kept: on macOS the focused window is the sender |
| `notificationBadge` | Kept: app-wide badge |
| `sendAll`, `destroyAll`, `syncAllAppearance` | Kept: broadcast |
| Preview `setMainWindow` owner | Kept: `"app"` owner from #1341 |
| `mainWindowBounds`, `flushMainWindowBounds` | Kept: hub bounds, #1345 |
| `ensureMain`, `createMain`, `revealOrCreateMain`, `createMainIfBackendReady`, `showConnectingSplash` | Kept: hub creation, #1343 |

## Why

Upstream binds IPC answers, dialogs, captures, and activation to the single hub window.

| Flow | Before, with project windows |
| --- | --- |
| Dialog from a project window | Parented to the hub |
| Fullscreen state | Read from the hub |
| One window's cleanup | Cleared `t3 app` activation for all |

Closes #1342

### Close conditions

- [x] Audit: every `main`-style consumer resolved
- [x] Fullscreen: targets the sender
- [x] Async: dialog, capture return to requester
- [x] Activation: readiness is per sender
- [x] Close one: others keep activation
- [x] Exclusions: no splash, DevTools, or popup

| Condition | Evidence in `apps/desktop/src/electron/WindowTargets.fork.test.ts` |
| --- | --- |
| Audit | Audit table above; "an IPC request resolves every main-style lookup to the sending window" |
| Fullscreen | "fullscreen state is read from the sender, not the hub", through `DesktopIpc.handleSync` |
| Async | "a dialog opened from a window is owned by that window while another has focus"; "every event of one capture reaches the window its request pinned"; "the capture reveals and activates the window it pinned"; "an interrupted capture releases its pinned window" |
| Activation | "a request goes to the most recently focused ready window"; "with the focused window not ready, a request walks focus recency"; "a renderer main did not register cannot clear the app windows" |
| Close one | "closing one ready window leaves the others activated"; "losing the window handling a request fails only that request" |
| Exclusions | "an app-wide request resolves to the most recently focused app window only": a focused unregistered window and an unregistered sender both resolve to the last app window |

### Live checks (host, not run here)

| Check | Steps and expectation |
| --- | --- |
| Fullscreen | Fullscreen a project window and reload it; its chrome reads fullscreen and the hub's does not |
| Dialog | Focus the hub, then pick a folder from a project window; the sheet parents to the project window |
| Capture | Snap shot with a project window last focused; the animation and result land there |
| Activation | Open the hub and a project, close the project, run `t3 app`; the request still completes |
| Exclusions | Focus detached DevTools, then use a menu action; it reaches the last app window |

### Swap proof

Temporary reverts at `f38334a280`, not committed:

| Reverted | `WindowTargets.fork.test.ts` |
| --- | --- |
| `ElectronWindow.ts` and `DesktopIpc.ts` hooks | 6 of 8 fail |
| `setRendererReady` hook | 3 of 8 fail (every activation case) |

### Checks

```text
vp test run <12 desktop files>  → exit 0 (12 files, 156 tests)
vp run fork:ci                  → exit 0 (scripts: 59 files, 648 tests)
vpr typecheck                   → exit 0
vp run knip:check               → exit 0
```

**Note:** `activate` now reveals the most recent window, not the hub, which also affects the dock behaviour in #1343.

## UI Changes

None: this changes which window main targets and adds no UI.

## Checklist

- [x] Small and focused
- [x] Explains what and why
- [x] Screenshots: n/a, no UI change
- [x] Video: n/a, no motion

Model and harness: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- c3c9d16 feat(desktop): window requests target the sender or the most recent window
- 14073c5 fixup! feat(desktop): window requests target the sender or the most recent window
- c74e5dc feat(desktop): window requests target the sender or the most recent window
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

Every window request resolves through one table in `apps/desktop/src/window/WindowDispatch.fork.ts`.
The startup drain and the second-instance buffer use the same table.

| Request | Behavior |
| --- | --- |
| New Window (menu, palette, key) | create at `/`, all projects |
| Open in New Window (row, thread) | create at that route, seed filter |
| Second launch, dock | focus most recent, else create |
| Deep link, `--project` | reuse a window showing it, else create |
| Focus window by id | focus if open; never resurrect |
| OAuth callback | unchanged |

- **Surfaces:** File > New Window, palette action, thread menu item
- **Bindings:** `window.new` on `mod+shift+w`
- **Rename:** `project.openWindow` becomes `window.openInNew`
- **Migration:** saved bindings with the old id load unchanged
- **IPC:** `openProjectWindow` replaced by `requestWindow`
- **Seed:** New Window seeds `"all-projects"`; Open in New Window and deep links seed their project
- **Recency:** a second launch focuses the first of `windowsByRecency`
- **Scope guard:** kept; a project window that leaves its project still closes, and closes itself rather than the first window sharing its project
- **List routes:** Issues and Pull requests keep a project window in its project from a shared page
- **Other project's item:** an issue or pull request link from another project widens the list to all projects, reading the window's project when the pathname names none
- **Guard close:** skipped when the window is already destroyed by the time the hub is revealed
- **Queued second launch:** drained after boot, it only creates main when none exists and never reveals a window

## Why

Closes #1343

Closes #1344

Window requests had separate paths with different reuse rules.
One table makes each row testable and keys windows by `WindowId` only.

### Issue work route

Issue work from `/project/<env>/<project>/issues` drafts at `/project/<env>/<project>/draft/<id>`, which the guard allows.
The route that bounced was the hub `/issues`.
Issues, from a shared page such as `/settings` in a project window, read the project from the pathname, found none, and navigated to `/issues`.
The guard then revealed the hub and closed the window.
`listRouteTarget` now falls back to the window's own project, so Issues and Pull requests stay on the project routes.
#179 left the route unconfirmed, and no desktop log still holds a `project window left its scope` line.
The trace came from the source; live proof then confirmed the fixed route (steps 70–74 below).

### Known limits

Refs #1345

- **Update relaunch:** extra hub or same-project windows are dropped; `DesktopWindowSession` dedups by identity and the startup drain restores through `getOrCreate`
- **New Window as hub:** when main closes, a New Window becomes main, but its bounds are never saved

## Close conditions

- [x] Table: each row has a focused test
  - `WindowDispatch.fork.test.ts`: one test per row, plus argv classification
- [x] Binding: `mod+shift+w`, conflict-checked
  - `keybindings.fork.test.ts`: no other default owns the chord
- [x] `mod+alt+o`: renamed; now always opens a new window, per the table
  - live: a second press creates a second window where `openIdentity` reused one (step 41)
  - migration of existing `keybindings.json`: unit-tested, not live
- [x] Issue work: thread opens in the source window
  - live: steps 70–74; issues from settings stay in the project, the draft opens there, and the guard closes only itself
  - `DesktopWindow.fork.test.ts`: issue work stays in its window; another project is still guarded
  - `projectRoutes.test.ts`: lists from a shared page stay in the window's project
- [x] Seed (#1344): New Window opens on all projects; Open in New Window opens on its project
  - live: New Window by key, menu, and palette (steps 20–22); Open in New Window by row and thread menu (steps 30–31)
  - `DesktopWindow.fork.test.ts`: the seed preload argument per request
- [x] Deep link: reuses a window showing the project, else creates one
  - live: steps 50–51
- [x] Second launch: focuses the most recent window
  - live: steps 63 and 65; Linux has no dock activate

Live proof ran at `025eb14c81` on a client-only dev Electron over a seeded copy, 6/6 pass, no stray windows.
Evidence: `.dump/evidence/1369-live/` on the host, indexed by `transcript.json`; step N is its `N-*` files.

## Checks

```text
vp test run (desktop window/ipc/app/electron) → exit 0, 60 files, 409 tests
vp test run (web links, routes, sidebar)      → exit 0, 17 files, 539 tests
vpr typecheck                            → exit 0
vp run knip:check                        → exit 0
vp run fork:ci                           → exit 0
```

Swap proofs, each reverted:

```diff
- open-in-new-window reuses a showing window  → 1 test fails
- activate goes to openPrimary                → 1 test fails
- migration returns input unchanged           → 2 tests fail
- New Window default mod+shift+n              → 2 tests fail
- dispatched window counted as primary        → 1 test fails
- guard never closes                          → 2 tests fail
- guard allows only the exact project route   → 1 test fails
- list route ignores the window's project     → 1 test fails
- seed not passed to the preload              → 1 test fails
- most recent read from the oldest window     → 1 test fails
- guard closes a destroyed window             → 1 test fails
- drained second launch takes the table row   → 1 test fails
- link scope ignores the window's project     → 1 test fails
```

## UI Changes

New menu, palette, and thread-menu items.
Screenshots stay in the host evidence directory above; they show private thread titles.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Model: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- 9d6e5df feat(desktop): New Window and deep links follow one dispatch table
- fd56cc1 fix(desktop): mod+alt+o reuses the project's window again
- 7d18eaf feat(desktop): New Window and deep links follow one dispatch table
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

| Area | Change |
| --- | --- |
| Manifest | v2: one row per window, `{ windowId, route, bounds?, workspace? }` |
| Restore | keyed by `windowId`; extra windows open via `createNew` |
| Bounds | the current main window saves them, and hands over on close |
| Token | only a window restored onto a workspace maps as `t3code-window-<WindowId>` |
| Rules | every rule staged on a token is removed when placement ends |
| Claims | exact title, else the one new client since show; atomic select and record |

## Why

Update relaunch dropped extra hub and same-project windows.
A New Window that became main never saved its bounds.

## Close conditions

### Manifest

- [x] **Route:** an invalid route falls back to `/`
- [x] **v1 project entry:** becomes a seeded window
- [x] **v1 hub entry:** becomes an all-projects window
- [x] **Bounds:** display-validated
- [x] **When:** update relaunch only
- [x] **Corrupt manifest:** still opens one window

### Placement

- [x] **Token:** the title carries the WindowId until claimed
- [x] **Claims:** serialized, never guessed
- [ ] **Live:** real Hyprland update relaunch

Placement boxes are proven against a faked compositor; the host arranges live proof.

### Notes

| Note | Detail |
| --- | --- |
| `project` field | required: a route alone cannot tell a project window from an all-projects one |
| Token scope | other windows keep their normal title, so user rules on it still fire |
| Rule cleanup | Lua: workspace and suppression rules disabled, handle globals cleared |
| Rule names | one compositor-wide counter; no name is ever reused |
| Legacy grammar | no unset for `suppress_event`, so tokens stage workspace only |
| No claim lock | select and record are atomic by construction |
| Known limit | all-projects windows without bounds open stacked at main's bounds |
| Known limit | two normal windows mapping at once, both retitled, stay unclaimed |

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes (none)
- [ ] I included a video for animation/interaction changes (none)

Closes #1345

Written by Claude Opus 5.5 in Claude Code, hosted by T3 Code.

## Fork trailers

Squashes:

- adf1305 feat(desktop): update relaunch restores every window from manifest v2 (#1577)
- d40e3ab fixup! feat(desktop): update relaunch restores every window from manifest v2
- 4d5a32f feat(desktop): update relaunch restores every window from manifest v2 (#1577)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

| Area | Change |
| --- | --- |
| Web routes | `project.$environmentId.$projectId*`, `projectRoutes.ts` removed |
| Web navigation | `threadRouteNavigation.ts` removed |
| Web scope | window scope, toggle, sidebar lock, `forcedProjectRef`, `scope=all` removed |
| Web chooser | multi-select chooser enabled in every window |
| Web landing | a window's first draft starts in its filter or seed |
| Web lists | a linked project outside the filter is still read |
| Web decoder | `/project/$` maps old links to ordinary routes |
| Web drafts | `:project:` buckets fold into route-free buckets |
| Desktop | scope guard, `projectWindowArgument.ts`, preload `projectWindowRef` removed |
| Desktop windows | project-link windows open at `/` with their seed |
| Scripts | hook scan accepts restored upstream comments and same-file export moves |
| Mobile | no project-route producers; unchanged |

### Decoder forms

| Old link | Lands on |
| --- | --- |
| `/project/E/P` | `/` |
| `/project/E/P/thread/T` | `/E/T` |
| `/project/E/P/draft/D` | `/draft/D` |
| `/project/E/P/issues` | `/issues` filtered to `P` |
| `/project/E/P/pull-requests` | `/pull-requests` filtered to `P` |
| `…?scope=all` | list without a project filter |
| Hash, slash-hash, escaped ids | same as above |
| Anything else | `/` |

## Why

A project window is a filter, not a route scope.
Issues and PR "All projects" now means the window's project filter.

## Close conditions

- [x] **Producers:** no code produces a project route
- [x] **Decoder:** old links reach ordinary routes
- [x] **Issues/PR:** "all" means all in this window
- [x] **Fold:** no upstream line deleted then restored

## Refs #1336

- **Drafts:** migrated on start, no text lost
- **Delta:** `project-windows` stays
- **Delta blockers:** live markers, history trailers

## Follow-ups

- **Out-of-filter links:** may not resolve on the list
- **Scan fix:** fork-meta tooling bundled here

Closes #1347

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Opus 5.5 via Claude Code; no live UI proof in scope.

## Fork trailers

Squashes:

- 84296d7 refactor(web): project routes retire behind a link decoder (#1578) (#1811)
- f9d2fd8 fixup! refactor(web): project routes retire behind a link decoder

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

The window's project filter now scopes lists and never hides running work.

| Area | Change |
| --- | --- |
| Outside thread | Header **Add project** adds its entry |
| Palette | Threads and projects scoped |
| Search all | Palette toggle reaches every project |
| New thread | Several projects: opens the scoped picker |
| Palette hint | `chat.new` hint only where it lands the same |
| Title | Web and hub desktop titles append the label |

**New thread rules:**

- **All:** upstream behavior
- **One project:** that project, viewed member first
- **Several:** picker; never a silent pick
- **Direct:** viewed project (shift+click, `chat.newLocal`)

**Gate:** the sidebar's chooser host.
The legacy sidebar and routes without the sidebar keep upstream behavior.

**Landing:** the index draft (#1347) keeps its most-recent pick inside the filter.
It is a landing, not a new-thread request, so it never asks.

## Why

A filtered window must narrow what it lists without closing threads or stopping work in other projects.
The filter is read only by lists, the chooser, the palette, and the header action.
Turns, previews, and terminals are keyed by thread, so filter changes never reach them.

### Surfaces

| Surface | Decision |
| --- | --- |
| Sidebar new thread | Filtered |
| Palette | Scoped, **Search all** toggle |
| `chat.new`, `chat.newLocal` | Filtered |
| Desktop title | Hub adds label; `t3code-window-` claim title held |
| Palette "New thread in" | Hint dropped when `chat.new` differs |
| Mobile | N/A: single-select, no palette or window title |
| Contracts | No wire change |
| Providers | N/A |

**Known limit:** Settings has no sidebar, so the title drops the label and the palette is unscoped there.

## UI Changes

Live Electron proof at `e6b38d7cc4`, before the rebase onto #1377:

![04-outside-thread-open-add-project.png](https://github.com/user-attachments/assets/3b586553-08d7-4555-929c-455b2d407e27)

![05-after-add-project.png](https://github.com/user-attachments/assets/fe9e2228-63ce-472d-ba18-c8c38feba934)

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

## Close conditions

- [x] **Outside:** opening one never closes it
- [x] **Add project:** adds its entry
- [x] **Search all:** reaches every project
- [x] **New thread:** never picks a member silently
- [x] **Title:** shows the filter label

Outside and Title: live proof at `e6b38d7cc4`; the rebase keeps both hooks.

Closes #1353

Model: Claude Opus 5.5 in Claude Code (T3 Code host).

## Fork trailers

Squashes:

- 5df6715 feat(web): the window filter scopes lists without hiding running work (#1811)
- 3915d9b fixup! feat(web): the window filter scopes lists without hiding running work

Fork-Domain: workspaces
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

- `WindowPlacement.placeAtMapFork` takes `maximize` and calls `window.maximize()` after `showInactive()`, so the window is already mapped under its staged workspace rule.
- `DesktopWindow.ts`: the placement branch runs before the upstream `mainWindowMaximized` maximize. That maximize and the focused reveal now run only on the path with no placement, which is unchanged.
- New test: a placed maximized window records `stage-rule`, `show-inactive`, `maximize` in that order, with no `show` and no `focus`.

## Why

Electron's `maximize()` shows a hidden window. The reveal trigger called it before the placement staged the Hyprland rule, so the window mapped on the focused workspace, then moved. On Hyprland `mainWindowMaximized` is normally `true`. A control run with it `false` mapped correctly and did not move.

Dev agent placement and restored windows both use `placeAtMapFork`, so this fix covers both. On a window that is already visible, Chromium's maximize only requests the maximized state and does not activate the window. The packaged re-run still has to prove that Hyprland keeps focus where it is.

Closes #261

- [ ] A project window maps on its intended workspace at first map, with no later move
- [ ] Neither monitor changes its active workspace when a project window opens

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Claude Code, claude-opus-5-5.

## Fork trailers

Squashes:

- c756f91 fix(desktop): a placed window maps on its workspace before it maximizes
- 3d3cb5a fix(desktop): windows opened together each claim their own Hyprland client
- 8f8346c fix(desktop): a placed window maps on its workspace before it maximizes
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
…1811)

The part of the createWindow reshape that stays in fork-owned files; its DesktopWindow.ts hooks folded into their owners.

Squashes:

- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor added a commit that referenced this pull request Oct 22, 2026
Squashes:

- b533812 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- b929e34 fixup! refactor(web): centralize thread route navigation
- 395c1f3 fixup! refactor(web): centralize thread route navigation
- 642f35f fixup! refactor(web): centralize thread route navigation
- d16eda2 refactor(web): centralize thread route navigation (#1480) (#29) (#1549) (#1569) (#1586) (#1438) (#1487) (#1809)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
Desktop app context menus now retain T3's theme, icons, and hierarchy instead of using native OS popups.

before: app action menus were rendered by Electron's native menu.

now: app action menus use the themed renderer, while spellcheck, media, and text-editing actions stay native.

![Themed Electron thread action menu](https://github.com/user-attachments/assets/07e7781e-a8c0-465a-9853-ce52dea23a4c)

## Mechanism

- App context menu requests stay in the renderer.
- The desktop host opens native menus only for Electron-exclusive actions.

Implemented with GPT-5.6 Codex in T3 Code.

Squashes:

- 3581900 fix(desktop): use themed app context menus (#1518) (#1572)
- f5adae0 fixup! fix(desktop): use themed app context menus
- 3d80224 fix(desktop): use themed app context menus (#1518) (#1572)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: upstream-fixes
Fork-Tier: bugfix
Fork-Upstreamable: yes
donjor added a commit that referenced this pull request Oct 22, 2026
…kspaces (#224)

Squashes:

- e07e59b feat(desktop): launch Electron from T3 Code actions into Hyprland workspaces (#224) (#224) (#395) (#1573)
- ead034c fix(fork): adapt fork seams to v0.0.46-nightly.20261005.2689
- f6701d4 feat(desktop): launch Electron from T3 Code actions into Hyprland workspaces (#224) (#224) (#395) (#1573)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
Squashes:

- 6aba10e Suspend hidden managed zmux terminal attachments (#1574) (#1811)
- 2023697 fixup! Suspend hidden managed zmux terminal attachments

Fork-Domain: zmux-estate
Fork-Tier: core
Fork-Upstreamable: no
donjor added a commit that referenced this pull request Oct 22, 2026
Squashes:

- c8a9569 feat(dev): open isolated test apps alongside stable (#1543)
- 7fd55b8 fix(desktop): an attached preview guest prepares in its own window's manager
- 865fe79 fix(desktop): desktop config and user-data fork tests compile against upstream's rework
- 5e3d870 feat(dev): open isolated test apps alongside stable (#1543)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: fork-meta
Fork-Tier: core
Fork-Upstreamable: no
donjor added a commit that referenced this pull request Oct 22, 2026
Squashes:

- 2a495c8 test(fork): move remaining pure fork titles behind the sibling boundary (#671) (#32)
- af2df9d fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1514) (#1514)
- 6ed88ba fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1553) (#1553)
- 1c2018e fixup! test(fork): move remaining pure fork titles behind the sibling boundary (#1589) (#1589)
- ab2ba58 test(fork): move remaining pure fork titles behind the sibling boundary (#671) (#32) (#1514) (#1553) (#1589)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: fork-meta
Fork-Tier: qol
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
…a second one

## What Changed

The desktop probes for a live local server before it spawns one.

- `packages/shared` gains a reader for `server-runtime.json`, so both the CLI and the desktop main process resolve a running server the same way.
- `t3 pair` gains `--json`, so a credential can be minted without parsing a QR code.
- The desktop runs in one of two backend modes. `managed` spawns and owns a backend. `client-only` attaches to one it did not start, never terminates it, and reconnects with backoff when it goes away. A live `server-runtime.json` flips a `managed` launch to `client-only`; `--backend-mode managed|client-only` overrides. Discovery is skipped in development.
- The web client pairs a client-only launch with the server it attached to, so a cold launch reaches an authenticated session with no pairing URL pasted by hand.
- The connection catalog is written mode `0600`. It holds every credential the app has been given.
- One `fork-sync` repair commit formats four files the `v0.0.41-nightly.20260910.1473` walk left unformatted. `vp check` is a required step and the fork runs CI only on a pull request, so the drift first surfaced here.

## Why

`t3code-backend.service` already serves this machine on `127.0.0.1:3773` and `https://t3.donjor.net`. Opening the desktop app started a second server against the same `~/.t3`, and quitting it took the service down with it.

The pool was already multi-instance and the renderer was already a pure client, so the gap was registration and ownership, not architecture.

## Verification

Run on the packaged AppImage built from the trial tree, against the real `t3code-backend.service`. The head since moved fork test cases into fork-owned `*.fork.test.ts` siblings and raised the budget rows; no product source changed after the trial.

| Behaviour | Result |
| --- | --- |
| Launch, service up, no flag | `client-only` / `existing-server`, one environment, no second listener |
| Authenticated session | bearer profile minted from `server-runtime.json`, no pairing paste |
| Catalog file mode | `664` before the write, `600` after |
| `systemctl --user restart` under an open window | service `3870853` to `1039036`, desktop PID unchanged, reconnected |
| Quit | service active, `https://t3.donjor.net` 200 |
| Launch, service down | `managed` / `settings`, spawned child on `127.0.0.1:3773`, exits with its parent |

```text
25 attach test files, 263 passed
typecheck: desktop, contracts, web, client-runtime — exit 0
vp run --filter @t3tools/web build — exit 0
vp run fork:delta --check — ok: 200 fork commits tagged
vp run fork:scan — 0 adopted authoring guard warnings
```

## UI Changes

None. The window is identical; only which backend it talks to changes.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes
- [x] I included a video for animation/interaction changes

Closes #742
Closes #743
Closes #744
Refs #735

Squashes:

- 44e8f89 feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593)
- a560ece fixup! feat(desktop): attach to a running local backend instead of spawning a second one
- d0eef76 feat(desktop): attach to a running local backend instead of spawning a second one (#1575) (#1516) (#1593)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: backend-attach
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

- **Drain module:** startup restore and launch intents move to `DesktopStartupDrain.fork.ts`.
  - Bounded queue: capacity 8, drops newest with a log line, never blocks.
  - Renderer readiness and backend readiness share one drain.
  - The drain opens the restore first, then each intent in arrival order, once.
  - It re-polls until empty, so an intent queued mid-drain still opens.
  - One winner claims the drain; later callers await it.
  - A failed open is a typed `ElectronWindowCreateError`.
  - `markNotReady` re-arms it after a backend restart.
- **Second-instance buffer:** `DesktopSecondInstanceBuffer.fork.ts` listens from single-instance lock acquisition.
  - It holds launches that arrive before `DesktopClerk.configure` registers the real listener.
  - It replays them after the first launch's argv, in order.
- **Client-only snapshot:** client-only bootstrap now runs `snapShot.initialize`.

## Why

- **Client-only readiness:** client-only desktop never called `handleBackendReady`, so restore and launch intents only drained in managed mode.
  - An update relaunch dropped every window.
  - A `--project` cold start opened the hub.
- **Early second launch:** a second launch arriving within about 0.1s of the first was dropped.
  - Electron emitted `second-instance` before any app listener existed.

## Close conditions

- [x] Relaunch: client-only update restores windows. Live client-only Electron relaunch with a recorded `window-session.json` reopened exactly `zmux` and `solar`, with no hub. Fork test "opens the restore before an intent queued during it".
- [x] Cold start: client-only `--project` opens it. Live: `--project <solar>` cold start produced one page, the solar project route, with no hub.
- [x] Order: queued intents drain once, in order. Live: second-instance launches at 0.05s ×3, 0.1s ×3 and 0.4s ×2 each opened both windows, A then B, with no duplicate; before the buffer, 0.05–0.1s dropped B in 8 of 8 trials. Fork tests: straggler during an open, followers admitted mid-open, buffer replay once.
- [x] Bound: the queue is bounded. `STARTUP_INTENT_QUEUE_CAPACITY = 8`; the second-instance buffer shares the cap (test "holds at most the startup queue capacity").
- [x] Tests: both backend modes covered. Both readiness paths call the same drain; drain and buffer fork tests are mode-agnostic, and live proof ran in client-only mode.

Live proof ran at `ccde0e21a7`, off-screen on an agent workspace.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

No UI changes: startup window-open behavior only; no screenshots.

Closes #1340

## Fork trailers

Squashes:

- d9d39f7 fix(desktop): client-only boots drain restore and launch intent
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

Main mints a random `WindowId` (UUID v4, `node:crypto`) per desktop window and passes it to the renderer as the `--t3code-window-id` preload argument. The registry, preview ownership, Hyprland claims, the snapshot sender check and the update-restore manifest key by it. Project identity stays as data on the window record.

- `WindowRegistry.fork.ts` (new): registry keyed by `WindowId`; identity lookup is a scan; a restored id is reused unless a live window holds it.
- `WindowId.fork.ts` (new): brand, preload argument, validation; package-free for the sandboxed preload.
- `ElectronWindow.ts`, `DesktopWindow.ts`: one-line `multi-window` hooks delegate to the registry; `getOrCreate` passes the reserved id to `create`.
- `WindowPolicy*.ts`, `Manager.ts`: preview owner is `WindowId | "app"`; the bridge exposes `windowId`.
- `HyprlandPlacement.ts`: claims and moves key by `WindowId`.
- `DesktopWindowSession.ts`, `DesktopStartupDrain.fork.ts`, `DesktopUpdates.ts`: the manifest carries an optional `windowId`; restore reopens under the old id. A manifest from before this change still restores.

## Why

A project key cannot name two windows on the same project, or a window whose project changes. An opaque id keys the window itself, so #1342 through #1345 can build on it. None of them is pre-built here.

Closes #1341

### Close conditions

- [x] Registry: keyed by `WindowId`
  - `ElectronWindow.fork.test.ts`: "keys each window by a minted WindowId, never by what it shows", "reuses a restored WindowId unless a live window already holds it"
- [x] Previews: ownership keyed by `WindowId`
  - `WindowPolicy.fork.test.ts`, `preview.fork.test.ts`: owner routing by `WindowId`; `preview.test.ts` asserts owner `[windowId]`, not the project identity; `DesktopWindow.test.ts` asserts `setWindow` owner `[testWindowId(1)]`
- [x] Hyprland: claim keyed by `WindowId`
  - `DesktopWindow.test.ts`: claims and workspace moves carry `[hubId, projectWindowId]` on restore and `testWindowId(1)` on launch
  - Unit tests only: the live claim log is debug-level
- [x] Demand, snapshot: keyed by `WindowId`
  - Snapshot: `snapShotSender.fork.ts` resolves the sender through `windowIdFor`; `snapShot.fork.test.ts` covers a registered project window and an unregistered one
  - Demand: published per window on its own `webContents`; no project key exists on that path
- [x] Reload: id survives a renderer reload
  - Live (host, `.dump/evidence/1364-live/`): menu Reload (`22-ctrl-r.txt`), `forcefullyCrashRenderer` (`23-crash.txt`) and update restore (`40-*`, `41-renderer-ids.txt`) all keep the id
  - Unit: `DesktopWindow.fork.test.ts` pins the id across `render-process-gone` recovery; `WindowPolicy.fork.test.ts` reads the id from argv and leaves it unset when absent

### Swap proof (each reverted with `git checkout -- <file>`)

```diff
- registry keys by identity key         → ElectronWindow.fork.test 2 failed
- id dropped from preload arguments     → DesktopWindow.test 3 failed
- drain ignores entry.windowId          → DesktopWindow.test 1 failed
- manifest read drops windowId          → DesktopWindowSession.test 2 failed
- Hyprland claim keyed by identity      → DesktopWindow.test 1 failed
- id dropped (crash recovery test)      → DesktopWindow.fork.test 1 failed
- preview owner keyed by identity       → DesktopWindow.test 1 failed
```

### Checks

```
vp test run <targeted desktop dirs>   → exit 0 (64 files, 700 tests)
vp run fork:ci                        → exit 0 (scripts 59 files, 648 tests)
vpr typecheck                         → exit 0
vp run knip:check                     → exit 0
```

### Note

The crash-recovery case lives in `DesktopWindow.fork.test.ts`: the fork:ci upstream-test rule refuses a new case in `DesktopWindow.test.ts`. `registerMain` now reuses a registered window's id and never mints one the preload cannot see.

## UI Changes

None. Main-process window keying only.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [x] I included before/after screenshots for any UI changes (n/a: no UI change)
- [x] I included a video for animation/interaction changes (n/a: no motion)

Model and harness: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- ef5a421 feat(desktop): key desktop windows by an opaque WindowId (#1576) (#1588) (#1590)
- ee337a4 fixup! feat(desktop): key desktop windows by an opaque WindowId
- 3ccc7bc fixup! feat(desktop): key desktop windows by an opaque WindowId
- 69a9cc5 fixup! feat(desktop): key desktop windows by an opaque WindowId
- cd0487c feat(desktop): key desktop windows by an opaque WindowId (#1576) (#1588) (#1590)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
donjor pushed a commit that referenced this pull request Oct 22, 2026
…indow

## What Changed

Every "main" window lookup in main now resolves by one rule.

| Request | Target |
| --- | --- |
| From a renderer (IPC) | The sender window |
| App-wide (menu, dock, `t3 app`, capture) | Most recently focused app window |

Only windows that main registered can be targets, so the splash, DevTools, and popups never are.
The upstream fallback runs only while no window is registered yet.

| File | Change |
| --- | --- |
| `WindowTargets.fork.ts` (new) | `IpcRequester` reference; `DesktopIpc` provides the sender around every invoke and sync handler |
| `WindowRegistry.fork.ts` | Focus recency per `WindowId`, dropped on close; `requestTarget` is the sender if registered, else the most recent window; `windowsByRecency` lists windows newest first |
| `ElectronWindow.ts` | `main`, `currentMainOrFirst`, `focusedMainOrFirst` return `requestTarget` first |
| `DesktopWindow.ts` | `activate` and `prepareCaptureReveal` use the capture's pinned window while a capture is acquiring, else the target; hub creation stays hub-only |
| `SnapShotTarget.fork.ts` (new) | A capture pins its window on `requested`; later events, the reveal, the post-capture activation, and the Linux destination window follow the pin. The pin is released on `ready`, `failed`, or when the capture fiber ends, interruption included |
| `DesktopAppActivationRenderers.fork.ts` (new) | Readiness per sender window; a request walks the ready windows in focus-recency order; an unregistered sender is ignored and never clears readiness; losing a window fails only the request it was handling |

### Audit

| Consumer | Resolves to |
| --- | --- |
| `ElectronWindow.main`, `currentMainOrFirst`, `focusedMainOrFirst` | Sender, else most recent (hooked once here) |
| `getWindowFullscreenState` (sync IPC) | Sender |
| `pickFolder`, `pickProjectFavicon`, `pickThemeFiles` owner | Sender |
| `showContextMenu` | Sender |
| `pasteAsText` main check | Sender: project windows can now paste as text |
| `ensureTrustedSnapShotSender` | Sender; the registered-window fallback is now redundant but kept |
| `DesktopSshPasswordPrompts.request` | Requester inside IPC, else most recent |
| `DesktopAppActivation.setRendererReady` | Per registered sender; an unregistered sender is ignored |
| `DesktopWindow.activate` (dock, `t3 app`) | Most recent |
| `DesktopWindow.activate` (post-capture) | Window pinned at `requested` |
| `prepareCaptureReveal` | Window pinned at `requested` |
| `dispatchMenuAction`, `zoomMain` | Most recent |
| `dispatchSnapShotEvent` | Window pinned at `requested` |
| `DesktopSnapShot` `destinationWindow` | Window pinned at `requested` |
| `openSystemSettings`, snapshot permission and setup owners | Kept: on macOS the focused window is the sender |
| `notificationBadge` | Kept: app-wide badge |
| `sendAll`, `destroyAll`, `syncAllAppearance` | Kept: broadcast |
| Preview `setMainWindow` owner | Kept: `"app"` owner from #1341 |
| `mainWindowBounds`, `flushMainWindowBounds` | Kept: hub bounds, #1345 |
| `ensureMain`, `createMain`, `revealOrCreateMain`, `createMainIfBackendReady`, `showConnectingSplash` | Kept: hub creation, #1343 |

## Why

Upstream binds IPC answers, dialogs, captures, and activation to the single hub window.

| Flow | Before, with project windows |
| --- | --- |
| Dialog from a project window | Parented to the hub |
| Fullscreen state | Read from the hub |
| One window's cleanup | Cleared `t3 app` activation for all |

Closes #1342

### Close conditions

- [x] Audit: every `main`-style consumer resolved
- [x] Fullscreen: targets the sender
- [x] Async: dialog, capture return to requester
- [x] Activation: readiness is per sender
- [x] Close one: others keep activation
- [x] Exclusions: no splash, DevTools, or popup

| Condition | Evidence in `apps/desktop/src/electron/WindowTargets.fork.test.ts` |
| --- | --- |
| Audit | Audit table above; "an IPC request resolves every main-style lookup to the sending window" |
| Fullscreen | "fullscreen state is read from the sender, not the hub", through `DesktopIpc.handleSync` |
| Async | "a dialog opened from a window is owned by that window while another has focus"; "every event of one capture reaches the window its request pinned"; "the capture reveals and activates the window it pinned"; "an interrupted capture releases its pinned window" |
| Activation | "a request goes to the most recently focused ready window"; "with the focused window not ready, a request walks focus recency"; "a renderer main did not register cannot clear the app windows" |
| Close one | "closing one ready window leaves the others activated"; "losing the window handling a request fails only that request" |
| Exclusions | "an app-wide request resolves to the most recently focused app window only": a focused unregistered window and an unregistered sender both resolve to the last app window |

### Live checks (host, not run here)

| Check | Steps and expectation |
| --- | --- |
| Fullscreen | Fullscreen a project window and reload it; its chrome reads fullscreen and the hub's does not |
| Dialog | Focus the hub, then pick a folder from a project window; the sheet parents to the project window |
| Capture | Snap shot with a project window last focused; the animation and result land there |
| Activation | Open the hub and a project, close the project, run `t3 app`; the request still completes |
| Exclusions | Focus detached DevTools, then use a menu action; it reaches the last app window |

### Swap proof

Temporary reverts at `f38334a280`, not committed:

| Reverted | `WindowTargets.fork.test.ts` |
| --- | --- |
| `ElectronWindow.ts` and `DesktopIpc.ts` hooks | 6 of 8 fail |
| `setRendererReady` hook | 3 of 8 fail (every activation case) |

### Checks

```text
vp test run <12 desktop files>  → exit 0 (12 files, 156 tests)
vp run fork:ci                  → exit 0 (scripts: 59 files, 648 tests)
vpr typecheck                   → exit 0
vp run knip:check               → exit 0
```

**Note:** `activate` now reveals the most recent window, not the hub, which also affects the dock behaviour in #1343.

## UI Changes

None: this changes which window main targets and adds no UI.

## Checklist

- [x] Small and focused
- [x] Explains what and why
- [x] Screenshots: n/a, no UI change
- [x] Video: n/a, no motion

Model and harness: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- c3c9d16 feat(desktop): window requests target the sender or the most recent window
- 14073c5 fixup! feat(desktop): window requests target the sender or the most recent window
- c74e5dc feat(desktop): window requests target the sender or the most recent window
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

Every window request resolves through one table in `apps/desktop/src/window/WindowDispatch.fork.ts`.
The startup drain and the second-instance buffer use the same table.

| Request | Behavior |
| --- | --- |
| New Window (menu, palette, key) | create at `/`, all projects |
| Open in New Window (row, thread) | create at that route, seed filter |
| Second launch, dock | focus most recent, else create |
| Deep link, `--project` | reuse a window showing it, else create |
| Focus window by id | focus if open; never resurrect |
| OAuth callback | unchanged |

- **Surfaces:** File > New Window, palette action, thread menu item
- **Bindings:** `window.new` on `mod+shift+w`
- **Rename:** `project.openWindow` becomes `window.openInNew`
- **Migration:** saved bindings with the old id load unchanged
- **IPC:** `openProjectWindow` replaced by `requestWindow`
- **Seed:** New Window seeds `"all-projects"`; Open in New Window and deep links seed their project
- **Recency:** a second launch focuses the first of `windowsByRecency`
- **Scope guard:** kept; a project window that leaves its project still closes, and closes itself rather than the first window sharing its project
- **List routes:** Issues and Pull requests keep a project window in its project from a shared page
- **Other project's item:** an issue or pull request link from another project widens the list to all projects, reading the window's project when the pathname names none
- **Guard close:** skipped when the window is already destroyed by the time the hub is revealed
- **Queued second launch:** drained after boot, it only creates main when none exists and never reveals a window

## Why

Closes #1343

Closes #1344

Window requests had separate paths with different reuse rules.
One table makes each row testable and keys windows by `WindowId` only.

### Issue work route

Issue work from `/project/<env>/<project>/issues` drafts at `/project/<env>/<project>/draft/<id>`, which the guard allows.
The route that bounced was the hub `/issues`.
Issues, from a shared page such as `/settings` in a project window, read the project from the pathname, found none, and navigated to `/issues`.
The guard then revealed the hub and closed the window.
`listRouteTarget` now falls back to the window's own project, so Issues and Pull requests stay on the project routes.
#179 left the route unconfirmed, and no desktop log still holds a `project window left its scope` line.
The trace came from the source; live proof then confirmed the fixed route (steps 70–74 below).

### Known limits

Refs #1345

- **Update relaunch:** extra hub or same-project windows are dropped; `DesktopWindowSession` dedups by identity and the startup drain restores through `getOrCreate`
- **New Window as hub:** when main closes, a New Window becomes main, but its bounds are never saved

## Close conditions

- [x] Table: each row has a focused test
  - `WindowDispatch.fork.test.ts`: one test per row, plus argv classification
- [x] Binding: `mod+shift+w`, conflict-checked
  - `keybindings.fork.test.ts`: no other default owns the chord
- [x] `mod+alt+o`: renamed; now always opens a new window, per the table
  - live: a second press creates a second window where `openIdentity` reused one (step 41)
  - migration of existing `keybindings.json`: unit-tested, not live
- [x] Issue work: thread opens in the source window
  - live: steps 70–74; issues from settings stay in the project, the draft opens there, and the guard closes only itself
  - `DesktopWindow.fork.test.ts`: issue work stays in its window; another project is still guarded
  - `projectRoutes.test.ts`: lists from a shared page stay in the window's project
- [x] Seed (#1344): New Window opens on all projects; Open in New Window opens on its project
  - live: New Window by key, menu, and palette (steps 20–22); Open in New Window by row and thread menu (steps 30–31)
  - `DesktopWindow.fork.test.ts`: the seed preload argument per request
- [x] Deep link: reuses a window showing the project, else creates one
  - live: steps 50–51
- [x] Second launch: focuses the most recent window
  - live: steps 63 and 65; Linux has no dock activate

Live proof ran at `025eb14c81` on a client-only dev Electron over a seeded copy, 6/6 pass, no stray windows.
Evidence: `.dump/evidence/1369-live/` on the host, indexed by `transcript.json`; step N is its `N-*` files.

## Checks

```text
vp test run (desktop window/ipc/app/electron) → exit 0, 60 files, 409 tests
vp test run (web links, routes, sidebar)      → exit 0, 17 files, 539 tests
vpr typecheck                            → exit 0
vp run knip:check                        → exit 0
vp run fork:ci                           → exit 0
```

Swap proofs, each reverted:

```diff
- open-in-new-window reuses a showing window  → 1 test fails
- activate goes to openPrimary                → 1 test fails
- migration returns input unchanged           → 2 tests fail
- New Window default mod+shift+n              → 2 tests fail
- dispatched window counted as primary        → 1 test fails
- guard never closes                          → 2 tests fail
- guard allows only the exact project route   → 1 test fails
- list route ignores the window's project     → 1 test fails
- seed not passed to the preload              → 1 test fails
- most recent read from the oldest window     → 1 test fails
- guard closes a destroyed window             → 1 test fails
- drained second launch takes the table row   → 1 test fails
- link scope ignores the window's project     → 1 test fails
```

## UI Changes

New menu, palette, and thread-menu items.
Screenshots stay in the host evidence directory above; they show private thread titles.

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Model: Claude Opus 5.5, Claude Code in T3 Code.

## Fork trailers

Squashes:

- 9d6e5df feat(desktop): New Window and deep links follow one dispatch table
- fd56cc1 fix(desktop): mod+alt+o reuses the project's window again
- 7d18eaf feat(desktop): New Window and deep links follow one dispatch table
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

| Area | Change |
| --- | --- |
| Manifest | v2: one row per window, `{ windowId, route, bounds?, workspace? }` |
| Restore | keyed by `windowId`; extra windows open via `createNew` |
| Bounds | the current main window saves them, and hands over on close |
| Token | only a window restored onto a workspace maps as `t3code-window-<WindowId>` |
| Rules | every rule staged on a token is removed when placement ends |
| Claims | exact title, else the one new client since show; atomic select and record |

## Why

Update relaunch dropped extra hub and same-project windows.
A New Window that became main never saved its bounds.

## Close conditions

### Manifest

- [x] **Route:** an invalid route falls back to `/`
- [x] **v1 project entry:** becomes a seeded window
- [x] **v1 hub entry:** becomes an all-projects window
- [x] **Bounds:** display-validated
- [x] **When:** update relaunch only
- [x] **Corrupt manifest:** still opens one window

### Placement

- [x] **Token:** the title carries the WindowId until claimed
- [x] **Claims:** serialized, never guessed
- [ ] **Live:** real Hyprland update relaunch

Placement boxes are proven against a faked compositor; the host arranges live proof.

### Notes

| Note | Detail |
| --- | --- |
| `project` field | required: a route alone cannot tell a project window from an all-projects one |
| Token scope | other windows keep their normal title, so user rules on it still fire |
| Rule cleanup | Lua: workspace and suppression rules disabled, handle globals cleared |
| Rule names | one compositor-wide counter; no name is ever reused |
| Legacy grammar | no unset for `suppress_event`, so tokens stage workspace only |
| No claim lock | select and record are atomic by construction |
| Known limit | all-projects windows without bounds open stacked at main's bounds |
| Known limit | two normal windows mapping at once, both retitled, stay unclaimed |

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes (none)
- [ ] I included a video for animation/interaction changes (none)

Closes #1345

Written by Claude Opus 5.5 in Claude Code, hosted by T3 Code.

## Fork trailers

Squashes:

- adf1305 feat(desktop): update relaunch restores every window from manifest v2 (#1577)
- d40e3ab fixup! feat(desktop): update relaunch restores every window from manifest v2
- 4d5a32f feat(desktop): update relaunch restores every window from manifest v2 (#1577)
- faa53bd refactor(multi-window): createWindow seams become marked insertions (#1811) (#1811)

Fork-Domain: multi-window
Fork-Tier: core
donjor pushed a commit that referenced this pull request Oct 22, 2026
## What Changed

| Area | Change |
| --- | --- |
| Web routes | `project.$environmentId.$projectId*`, `projectRoutes.ts` removed |
| Web navigation | `threadRouteNavigation.ts` removed |
| Web scope | window scope, toggle, sidebar lock, `forcedProjectRef`, `scope=all` removed |
| Web chooser | multi-select chooser enabled in every window |
| Web landing | a window's first draft starts in its filter or seed |
| Web lists | a linked project outside the filter is still read |
| Web decoder | `/project/$` maps old links to ordinary routes |
| Web drafts | `:project:` buckets fold into route-free buckets |
| Desktop | scope guard, `projectWindowArgument.ts`, preload `projectWindowRef` removed |
| Desktop windows | project-link windows open at `/` with their seed |
| Scripts | hook scan accepts restored upstream comments and same-file export moves |
| Mobile | no project-route producers; unchanged |

### Decoder forms

| Old link | Lands on |
| --- | --- |
| `/project/E/P` | `/` |
| `/project/E/P/thread/T` | `/E/T` |
| `/project/E/P/draft/D` | `/draft/D` |
| `/project/E/P/issues` | `/issues` filtered to `P` |
| `/project/E/P/pull-requests` | `/pull-requests` filtered to `P` |
| `…?scope=all` | list without a project filter |
| Hash, slash-hash, escaped ids | same as above |
| Anything else | `/` |

## Why

A project window is a filter, not a route scope.
Issues and PR "All projects" now means the window's project filter.

## Close conditions

- [x] **Producers:** no code produces a project route
- [x] **Decoder:** old links reach ordinary routes
- [x] **Issues/PR:** "all" means all in this window
- [x] **Fold:** no upstream line deleted then restored

## Refs #1336

- **Drafts:** migrated on start, no text lost
- **Delta:** `project-windows` stays
- **Delta blockers:** live markers, history trailers

## Follow-ups

- **Out-of-filter links:** may not resolve on the list
- **Scan fix:** fork-meta tooling bundled here

Closes #1347

## Checklist

- [x] This PR is small and focused
- [x] I explained what changed and why
- [ ] I included before/after screenshots for any UI changes
- [ ] I included a video for animation/interaction changes

Opus 5.5 via Claude Code; no live UI proof in scope.

## Fork trailers

Squashes:

- 84296d7 refactor(web): project routes retire behind a link decoder (#1578) (#1811)
- f9d2fd8 fixup! refactor(web): project routes retire behind a link decoder

Fork-Domain: multi-window
Fork-Tier: core
Fork-Upstreamable: no
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant