Fix detached-window IPC scoping and pane-transfer session loss
- capabilities/default.json: extend window scope to "pane-window-*" so detached windows can invoke/listen (fixes blank panes B2-B5). - App.tsx: memoize the destructive take_pending_window_init read at module scope so React StrictMode's double mount-effect doesn't consume the transfer payload twice and lose the adopted PTY session. - lib.rs: add `use tauri::Manager;` for Window::app_handle() in on_window_event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
681d15fdc3
commit
bea6cf2977
4 changed files with 54 additions and 2 deletions
31
memory.md
31
memory.md
|
|
@ -87,6 +87,37 @@ Two big features the user asked for in one session. Three commits on `main`: `1a
|
|||
|
||||
**Phase 2 verification needed** (user, on Windows host):
|
||||
1. `cd D:\dev\tiletopia\src-tauri && cargo check` — the Rust changes have to compile. **Note: `Cargo.toml` lives in `src-tauri/`, NOT the project root** (Tauri layout). I got this wrong in the original verification steps; user had to point it out. Added a preflight-checks rule to global `~/claude/CLAUDE.md`. Watch in the check output for: tauri 2 `WebviewWindowBuilder::new` signature, `on_window_event` handler closure types, my `Arc<Self>` method receiver style on WindowsState.
|
||||
|
||||
**Uncommitted local fix (as of 2026-05-28 wrap-up):**
|
||||
|
||||
`src-tauri/src/lib.rs` has an added `use tauri::Manager;` import — needed because `Window::app_handle()` is a trait method (Manager trait) used in the new `on_window_event` handler. Same pattern as the `Emitter` trait stumble in v0.3.0. Cargo check went clean after this. **Not committed yet** — user wanted to smoke-test the feature first, then found the bug list below. Commit this fix at the same time as the bug-fix commit.
|
||||
|
||||
**Detached-window bug list (deferred — user will resume):**
|
||||
|
||||
Smoke test on Windows revealed bugs specific to detached (non-main) windows. Main window is unaffected.
|
||||
|
||||
- **B1** — Drag-out has no ghost image during drag (cosmetic, user OK with deferring).
|
||||
- **B2** — Detached window: transferred pane is blank, "idle" within 5s. No input, no output.
|
||||
- **B3** — Detached window: shell-picker swap (Ubuntu → PowerShell → Ubuntu) doesn't spawn a working terminal. Fresh `spawn_pane` call from the detached window — toolbar updates but no PTY output.
|
||||
- **B4** — Detached window: new tab (Ctrl+T or + button) creates the tab but no terminal. Same blank/idle symptom.
|
||||
- **B5** — Right-click "Move to new window" produces the same broken detached window as drag-out. Confirms the bug is detached-window-scoped, not gesture-scoped.
|
||||
- **B6** (control) — Main window: new tab, new pane, normal ops all work.
|
||||
|
||||
**Strongest single hypothesis** for B2–B5: **Tauri 2's capability system gates `invoke` and `listen` per window-label.** Default capability config in `src-tauri/capabilities/default.json` (or similar) usually scopes to `"windows": ["main"]`. Newly-built `pane-window-*` labels match nothing → all IPC and events silently fail. One config fix (add wildcard window pattern, or programmatically attach a capability to each new window before `.build()`) would explain ALL of B2-B5 in one go.
|
||||
|
||||
**Where to look first when resuming:**
|
||||
1. `src-tauri/capabilities/*.json` — read the existing capability config to confirm scoping.
|
||||
2. Try `"windows": ["main", "pane-window-*"]` (Tauri 2 supports glob patterns in capability window targets).
|
||||
3. If that doesn't work: `AppHandle::add_capability(...)` on the new window before `.build()` in `commands.rs::create_pane_window`.
|
||||
4. Verify by re-testing B4 first (simplest: fresh new tab in a detached window — needs only `invoke("spawn_pane")` and `listen("pane://...")` to work).
|
||||
|
||||
**RESOLVED 2026-05-28 (resume session) — two root causes, both fixed:**
|
||||
|
||||
- **B2–B5 (blank/dead detached windows) = the capability hypothesis, confirmed.** `src-tauri/capabilities/default.json` had `"windows": ["main"]`; detached labels are `pane-window-<micros>` (commands.rs:122) → matched nothing → every `invoke`/`listen` silently denied. Fix: `"windows": ["main", "pane-window-*"]`. Tauri 2 glob pattern works; one line cleared all four. (App-defined commands aren't individually permission-gated — they're available to any window the capability is *applied* to, i.e. listed in `windows`.)
|
||||
- **Session-loss-on-adopt (surfaced after B2–B5 cleared) = destructive read × StrictMode.** Once IPC worked, drag-out still spawned a FRESH pty (new id, tab named "Default", status `alive` not `adopted`) instead of adopting. Cause: `take_pending_window_init` is a **destructive** backend read (`by_label.remove`); React StrictMode runs the mount effect twice in dev — pass 1 consumed the payload then bailed on the `cancelled` flag, pass 2 got `null` → fell back to `singletonEnvelope` (fresh "Default" + fresh spawn). The `cancelled`-flag pattern guards against *using* stale async results but cannot un-consume a destructive backend call. Fix: module-level memoized `consumePendingWindowInit()` in App.tsx so the take fires **exactly once per window** and both StrictMode passes share the payload. Dev-only symptom (prod StrictMode doesn't double-invoke effects) but fixed for robustness. **Lesson: any destructive/once-only backend read called from a mount effect must be memoized at module scope, not just guarded by `cancelled`.**
|
||||
- **Verified:** user confirmed adopt works (scrollback intact, same pane id, live input). `tsc -b` clean. B1 (drag ghost image) still deferred — cosmetic.
|
||||
- Committed together with the carried-over `use tauri::Manager;` lib.rs import.
|
||||
|
||||
2. `pnpm tauri dev` — smoke test:
|
||||
- Existing workspace loads as one tab named "Default" ✓ migrate
|
||||
- Ctrl+T spawns new tab with default-shell pane
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability set for wsl-mux spike",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "pane-window-*"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ mod window_state;
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
// `Manager` trait must be in scope to call `.app_handle()` on the `&Window`
|
||||
// passed to the `on_window_event` closure below. Same pattern as the
|
||||
// `Emitter` trait needed for `.emit()` (see 2026-05-26 PR-1 session log).
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::mcp::{McpServerHandle, McpState, PendingActions};
|
||||
use crate::pty::PtyManager;
|
||||
use crate::window_state::{PendingInits, WindowsState, MAIN_WINDOW_LABEL};
|
||||
|
|
|
|||
18
src/App.tsx
18
src/App.tsx
|
|
@ -42,6 +42,22 @@ const MAIN_WINDOW_LABEL = "main";
|
|||
* this window's state to the cross-window aggregator. */
|
||||
const CURRENT_WINDOW_LABEL = getCurrentWebviewWindow().label;
|
||||
const IS_MAIN_WINDOW = CURRENT_WINDOW_LABEL === MAIN_WINDOW_LABEL;
|
||||
|
||||
/** `take_pending_window_init` is a DESTRUCTIVE backend read (it removes the
|
||||
* entry). React StrictMode runs the mount effect twice in dev, so a plain
|
||||
* call would consume the payload on the first (cancelled) pass and hand the
|
||||
* second pass `null` — booting a fresh "Default" workspace and spawning a new
|
||||
* PTY instead of adopting the transferred one (session lost). Memoize the
|
||||
* promise at module scope so the backend take happens exactly once per window
|
||||
* and every effect pass awaits the same result. */
|
||||
let pendingInitOnce: Promise<Awaited<ReturnType<typeof takePendingWindowInit>>> | null =
|
||||
null;
|
||||
const consumePendingWindowInit = () => {
|
||||
if (!pendingInitOnce) {
|
||||
pendingInitOnce = takePendingWindowInit(CURRENT_WINDOW_LABEL);
|
||||
}
|
||||
return pendingInitOnce;
|
||||
};
|
||||
import {
|
||||
type TreeNode,
|
||||
type NodeId,
|
||||
|
|
@ -263,7 +279,7 @@ export default function App() {
|
|||
|
||||
if (!IS_MAIN_WINDOW) {
|
||||
try {
|
||||
const pending = await takePendingWindowInit(CURRENT_WINDOW_LABEL);
|
||||
const pending = await consumePendingWindowInit();
|
||||
if (pending) {
|
||||
try {
|
||||
const adoptedLeaf = JSON.parse(pending.leafJson) as LeafNode;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue