Idle filter: pivot per-distro → per-pane via TILETOPIA_PANE_ID env marker
Per-distro suppression (shipped earlier today) broke tiletopia's primary use case — multiple claude panes per distro means as soon as one runs claude, ALL Ubuntu panes go silent. Tested live: user couldn't reproduce idle on any pane because PID 46848 (their main session) tripped the gate. New mechanism, per-pane via env-var marker: 1. pty.rs tags every WSL spawn with TILETOPIA_PANE_ID=<id> as a Windows env var, plus WSLENV=...TILETOPIA_PANE_ID/u (appended to any pre- existing WSLENV) so the var forwards into the distro. Pane id is now reserved BEFORE build_command so the tag is available at spawn time. 2. probe.rs rewritten — is_watch_process_running(distro, pane_id) runs a bash one-liner that pgreps for each watched name, then for each PID checks /proc/<pid>/environ for the matching TILETOPIA_PANE_ID line. Env inheritance does the work: shell inherits from wsl.exe, claude inherits from shell. Cache keyed by (distro, pane_id). 3. Fail-safe INVERTED: probe failure now returns false (don't suppress) instead of true (suppress). A transient error should never silence the idle indicator permanently. Frontend catch updated to match. 4. LeafPane tracks PaneId in paneIdRef set by onPaneSpawned; idle ticks before spawn-completion pass 0, which won't match any real marker so the pane idles normally. Existing panes won't have the marker until respawned — they'll always show idle (since probe never matches). User opens fresh panes once after deploying this. Documented in memory.md follow-ups. pnpm check clean. Rust validation: cargo test --lib on Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d3474d33b0
commit
6772b8db37
6 changed files with 230 additions and 124 deletions
|
|
@ -117,23 +117,28 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
|
|||
// 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
|
||||
// the distro. If it is, the silence is "claude thinking / user reading",
|
||||
// not "nothing happening" — stay quiet. Probe is per-distro (not per-
|
||||
// pane: the inside-WSL PID isn't observable from Windows), so multiple
|
||||
// panes in the same distro will all suppress if claude is running in
|
||||
// any of them. Agreed trade-off; over-suppression beats the previous
|
||||
// always-notify behaviour.
|
||||
// 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 the remote box).
|
||||
// (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 treats that as fail-safe true.
|
||||
// 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) => {
|
||||
|
|
@ -177,15 +182,16 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
|
|||
// WSL path. Don't stack probes — one in flight per pane at a time.
|
||||
if (inFlight) return;
|
||||
inFlight = true;
|
||||
void isWatchProcessRunning(wslDistro)
|
||||
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 — treat silence
|
||||
// as expected and stay out of the idle set.
|
||||
// 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);
|
||||
|
|
@ -200,8 +206,9 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
|
|||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// Probe IPC errored — fail-safe to suppression (matches the Rust
|
||||
// side's own fail-safe).
|
||||
// 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);
|
||||
|
|
@ -243,6 +250,7 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
|
|||
|
||||
const onPaneSpawned = useCallback(
|
||||
(paneId: number) => {
|
||||
paneIdRef.current = paneId;
|
||||
orch.registerPaneId(leaf.id, paneId);
|
||||
},
|
||||
[orch.registerPaneId, leaf.id],
|
||||
|
|
@ -252,7 +260,10 @@ 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 () => orch.registerPaneId(leaf.id, null);
|
||||
return () => {
|
||||
paneIdRef.current = null;
|
||||
orch.registerPaneId(leaf.id, null);
|
||||
};
|
||||
}, [orch.registerPaneId, leaf.id]);
|
||||
|
||||
const onXtermFocus = useCallback(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue