Idle filter: suppress when watched process (claude) is running in distro
Probes wsl.exe -d <distro> -- pgrep -x claude before flagging a WSL pane idle, with a 3s per-distro cache on the Rust side. If claude is running anywhere in the distro, all panes in that distro stay out of the idle set (per-pane granularity is out of scope — PIDs aren't observable from Windows). PowerShell + SSH panes skip the probe and keep the legacy always-notify behaviour.
This commit is contained in:
parent
5b970f8b48
commit
f51033a142
7 changed files with 352 additions and 11 deletions
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { type LeafNode, resolveFontSize, type LeafShellSpec } from "./tree";
|
||||
import { useOrchestration } from "./orchestration";
|
||||
import XtermPane from "../../components/XtermPane";
|
||||
import type { SpawnSpec } from "../../ipc";
|
||||
import { isWatchProcessRunning, type SpawnSpec } from "../../ipc";
|
||||
import "./LeafPane.css";
|
||||
|
||||
const IDLE_THRESHOLD_MS = 5000;
|
||||
|
|
@ -116,8 +116,24 @@ 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
|
||||
// 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.
|
||||
//
|
||||
// PowerShell + SSH skip the probe and fall through to legacy behaviour
|
||||
// (PS has no portable `ps`; SSH processes live on the 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.
|
||||
const wslDistro = isWslPane ? (leaf.distro ?? "") : "";
|
||||
const onDataReceived = useCallback(() => {
|
||||
lastDataTimeRef.current = Date.now();
|
||||
setIsIdle((cur) => {
|
||||
|
|
@ -126,17 +142,81 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
|
|||
});
|
||||
}, [orch.reportLeafIdle, leaf.id]);
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => {
|
||||
// Guard against late-resolving probes after unmount or another tick
|
||||
// already shipping a fresher answer.
|
||||
let cancelled = false;
|
||||
let inFlight = false;
|
||||
|
||||
const tick = () => {
|
||||
const dt = Date.now() - lastDataTimeRef.current;
|
||||
const nowIdle = dt >= IDLE_THRESHOLD_MS;
|
||||
setIsIdle((cur) => {
|
||||
if (cur === nowIdle) return cur;
|
||||
orch.reportLeafIdle(leaf.id, nowIdle);
|
||||
return nowIdle;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [leaf.id, orch.reportLeafIdle]);
|
||||
|
||||
// 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;
|
||||
void isWatchProcessRunning(wslDistro)
|
||||
.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.
|
||||
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 — fail-safe to suppression (matches the Rust
|
||||
// side's own fail-safe).
|
||||
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]);
|
||||
// Clear from the app-level idle set when this pane unmounts.
|
||||
useEffect(() => {
|
||||
return () => orch.reportLeafIdle(leaf.id, false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue