Revert idle "claude foreground" filter — back to legacy 5s notify

Reverts in one combined commit:
- 9931a92 (inline pane_id + watch list into bash script)
- 6772b8d (pivot per-distro → per-pane via TILETOPIA_PANE_ID env)
- f51033a (original per-distro idle filter)

End-to-end probe never worked correctly against the real running app
even after fixing the wsl.exe-drops-positional-args bug. Probe script
ran fine in isolation but kept returning false-negative when called
through tiletopia's wsl.exe spawn. Rather than keep iterating, back
out cleanly — pane behaviour is now the original "go idle after 5s of
silence regardless of what's running."

memory.md session log notes the lessons for a future retry: don't ship
per-distro again (CLAUDE.md explicitly says multi-claude-per-distro is
the primary use case); prove the probe end-to-end before wiring into
the idle effect (a "Test probe" button in MCP panel would have caught
this in minutes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-26 18:33:11 +01:00
parent 9931a92c5f
commit 50fbd0e531
7 changed files with 27 additions and 486 deletions

View file

@ -39,19 +39,6 @@ export interface SshHost {
export const listDistros = (): Promise<string[]> => invoke("list_distros");
/** Ask the backend whether any built-in "watched" process (currently just
* `claude`) is running in THIS specific pane (not just somewhere in the
* distro). Per-pane detection works via a `TILETOPIA_PANE_ID` env marker
* injected at spawn see src-tauri/src/probe.rs. Cached per (distro,
* pane_id) for ~3s. Probe failures resolve to `false` (don't suppress)
* better to occasionally over-notify than permanently silence. Only
* meaningful for WSL panes; PS + SSH should skip this. */
export const isWatchProcessRunning = (
distro: string,
paneId: number,
): Promise<boolean> =>
invoke("is_watch_process_running", { distro, paneId });
export const spawnPane = (args: {
spec: SpawnSpec;
cols: number;

View file

@ -10,7 +10,7 @@ import {
import { type LeafNode, resolveFontSize, type LeafShellSpec } from "./tree";
import { useOrchestration } from "./orchestration";
import XtermPane from "../../components/XtermPane";
import { isWatchProcessRunning, type SpawnSpec } from "../../ipc";
import type { SpawnSpec } from "../../ipc";
import "./LeafPane.css";
const IDLE_THRESHOLD_MS = 5000;
@ -116,29 +116,8 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
// ---- idle detection ----------------------------------------------------
// Local boolean for the red border + status text on this pane; reported
// up to App via orch.reportLeafIdle for the titlebar's "N idle" badge.
//
// Filter: for WSL panes, before flagging idle we probe the backend to see
// if any "watched" process (currently just `claude`) is running in THIS
// pane specifically — per-pane, not per-distro. Per-pane is essential for
// tiletopia's primary use case (multiple claude sessions across panes in
// the same distro). The backend matches by reading `TILETOPIA_PANE_ID`
// out of each candidate process's `/proc/<pid>/environ` (the env var is
// injected at spawn time; see src-tauri/src/pty.rs WSLENV setup).
//
// PowerShell + SSH skip the probe and fall through to legacy behaviour
// (PS has no portable `ps`; SSH processes live on a remote box).
const lastDataTimeRef = useRef(Date.now());
const [isIdle, setIsIdle] = useState(false);
const isWslPane = leaf.shellKind === "wsl";
// Captures the distro name into the interval callback. Empty string when
// the leaf doesn't have one yet — the probe returns "not running" for
// empty input so the pane goes idle normally.
const wslDistro = isWslPane ? (leaf.distro ?? "") : "";
// Backend pane id (PaneId, the u64 used inside Rust). Set by the
// XtermPane onSpawn callback; null until the spawn round-trip completes.
// Idle ticks before that point pass 0 — won't match any real pane's
// TILETOPIA_PANE_ID env, so the probe returns false (no suppression).
const paneIdRef = useRef<number | null>(null);
const onDataReceived = useCallback(() => {
lastDataTimeRef.current = Date.now();
setIsIdle((cur) => {
@ -147,83 +126,17 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
});
}, [orch.reportLeafIdle, leaf.id]);
useEffect(() => {
// Guard against late-resolving probes after unmount or another tick
// already shipping a fresher answer.
let cancelled = false;
let inFlight = false;
const tick = () => {
const id = window.setInterval(() => {
const dt = Date.now() - lastDataTimeRef.current;
const nowIdle = dt >= IDLE_THRESHOLD_MS;
// Transitioning out of idle is unconditional — fresh output beats
// any probe answer.
if (!nowIdle) {
setIsIdle((cur) => {
if (!cur) return cur;
orch.reportLeafIdle(leaf.id, false);
return false;
});
return;
}
// Transitioning into idle. Non-WSL panes: report immediately (legacy
// behaviour). WSL panes: gate on the probe; suppress if a watched
// process is running in the distro.
if (!isWslPane) {
setIsIdle((cur) => {
if (cur) return cur;
orch.reportLeafIdle(leaf.id, true);
return true;
});
return;
}
// WSL path. Don't stack probes — one in flight per pane at a time.
if (inFlight) return;
inFlight = true;
const paneIdForProbe = paneIdRef.current ?? 0;
void isWatchProcessRunning(wslDistro, paneIdForProbe)
.then((suppress) => {
if (cancelled) return;
// If output arrived while the probe was in flight, the next tick
// (or onDataReceived) will reconcile; don't flip-flop here.
if (Date.now() - lastDataTimeRef.current < IDLE_THRESHOLD_MS) return;
if (suppress) {
// claude (or another watched proc) is running in THIS pane —
// treat the silence as expected; stay out of the idle set.
setIsIdle((cur) => {
if (!cur) return cur;
orch.reportLeafIdle(leaf.id, false);
return false;
});
} else {
setIsIdle((cur) => {
if (cur) return cur;
orch.reportLeafIdle(leaf.id, true);
return true;
});
}
})
.catch((e) => {
// Probe IPC errored — don't flip idle either way; next tick retries.
// The Rust side now also fails-safe to "not running" so the pane
// will flag idle eventually if the probe stays broken.
if (cancelled) return;
// eslint-disable-next-line no-console
console.debug("idle probe failed", e);
})
.finally(() => {
inFlight = false;
});
};
const id = window.setInterval(tick, 1000);
return () => {
cancelled = true;
clearInterval(id);
};
}, [leaf.id, orch.reportLeafIdle, isWslPane, wslDistro]);
setIsIdle((cur) => {
if (cur === nowIdle) return cur;
orch.reportLeafIdle(leaf.id, nowIdle);
return nowIdle;
});
}, 1000);
return () => clearInterval(id);
}, [leaf.id, orch.reportLeafIdle]);
// Clear from the app-level idle set when this pane unmounts.
useEffect(() => {
return () => orch.reportLeafIdle(leaf.id, false);
@ -250,7 +163,6 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
const onPaneSpawned = useCallback(
(paneId: number) => {
paneIdRef.current = paneId;
orch.registerPaneId(leaf.id, paneId);
},
[orch.registerPaneId, leaf.id],
@ -260,10 +172,7 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
// which broke broadcast routing (peers found, but their paneIds
// had been silently removed from the map).
useEffect(() => {
return () => {
paneIdRef.current = null;
orch.registerPaneId(leaf.id, null);
};
return () => orch.registerPaneId(leaf.id, null);
}, [orch.registerPaneId, leaf.id]);
const onXtermFocus = useCallback(