diff --git a/src/App.css b/src/App.css index 702340e..60dc3d9 100644 --- a/src/App.css +++ b/src/App.css @@ -70,6 +70,9 @@ color: #777; font-size: 11px; } +.layout-info .idle-info { + color: #d96060; +} .pane-wrap { flex: 1 1 auto; diff --git a/src/App.tsx b/src/App.tsx index f5fc214..51d0c7c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -239,6 +239,19 @@ export default function App() { setNotifications((ns) => ns.filter((n) => n.id !== id)); }, []); + // ---- per-pane idle aggregation (replaces toast spam) -------------------- + const [idleLeafIds, setIdleLeafIds] = useState>(() => new Set()); + const reportLeafIdle = useCallback((leafId: NodeId, idle: boolean) => { + setIdleLeafIds((prev) => { + if (idle && prev.has(leafId)) return prev; + if (!idle && !prev.has(leafId)) return prev; + const next = new Set(prev); + if (idle) next.add(leafId); + else next.delete(leafId); + return next; + }); + }, []); + // ---- header-drag swap --------------------------------------------------- const [dragSourceId, setDragSourceId] = useState(null); const [dragOverId, setDragOverId] = useState(null); @@ -283,6 +296,7 @@ export default function App() { beginHeaderDrag, setHeaderDragOver, endHeaderDrag, + reportLeafIdle, }), [ activeLeafId, @@ -301,6 +315,7 @@ export default function App() { beginHeaderDrag, setHeaderDragOver, endHeaderDrag, + reportLeafIdle, ], ); @@ -443,6 +458,12 @@ export default function App() { {leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"} + {idleLeafIds.size > 0 && ( + + {" · "} + {idleLeafIds.size} idle + + )} diff --git a/src/lib/layout/LeafPane.css b/src/lib/layout/LeafPane.css index fede6ce..390de89 100644 --- a/src/lib/layout/LeafPane.css +++ b/src/lib/layout/LeafPane.css @@ -17,6 +17,20 @@ .leaf.active.broadcasting { border-color: #ffb840; } +.leaf.idle { + border-color: #c84040; +} +/* active / broadcasting beats idle visually — when you're focused on a + pane (active), the blue tells you "you're here"; idle is implied. */ +.leaf.active.idle { + border-color: #5a8cd8; +} +.leaf.broadcasting.idle { + border-color: #e09838; +} +.leaf.active.broadcasting.idle { + border-color: #ffb840; +} .leaf.drag-source { opacity: 0.4; } @@ -154,6 +168,7 @@ } .pane-status.ok { color: #6c6; } .pane-status.err { color: #d66; } +.pane-status.idle { color: #d96060; } .pane-actions { display: flex; diff --git a/src/lib/layout/LeafPane.tsx b/src/lib/layout/LeafPane.tsx index 62a1996..f98dfa6 100644 --- a/src/lib/layout/LeafPane.tsx +++ b/src/lib/layout/LeafPane.tsx @@ -79,27 +79,33 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { }, [distroOpen]); // ---- 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. const lastDataTimeRef = useRef(Date.now()); - const notifiedThisIdleRef = useRef(false); + const [isIdle, setIsIdle] = useState(false); const onDataReceived = useCallback(() => { lastDataTimeRef.current = Date.now(); - notifiedThisIdleRef.current = false; - }, []); + setIsIdle((cur) => { + if (cur) orch.reportLeafIdle(leaf.id, false); + return false; + }); + }, [orch.reportLeafIdle, leaf.id]); useEffect(() => { const id = window.setInterval(() => { - if (notifiedThisIdleRef.current) return; const dt = Date.now() - lastDataTimeRef.current; - if (dt >= IDLE_THRESHOLD_MS) { - notifiedThisIdleRef.current = true; - const name = leaf.label ?? leaf.distro ?? "pane"; - orch.notify(`${name} is idle`); - } + const nowIdle = dt >= IDLE_THRESHOLD_MS; + setIsIdle((cur) => { + if (cur === nowIdle) return cur; + orch.reportLeafIdle(leaf.id, nowIdle); + return nowIdle; + }); }, 1000); return () => clearInterval(id); - // Depend on the stable notify function, not the whole orch object. - // orch is recreated every time activeLeafId/distros change; depending - // on it would tear down and rebuild this interval on every click. - }, [leaf.label, leaf.distro, orch.notify]); + }, [leaf.id, orch.reportLeafIdle]); + // Clear from the app-level idle set when this pane unmounts. + useEffect(() => { + return () => orch.reportLeafIdle(leaf.id, false); + }, [leaf.id, orch.reportLeafIdle]); // ---- broadcast --------------------------------------------------------- const onTerminalInput = useCallback( @@ -229,7 +235,7 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { return (
- {status} + {isIdle && statusOk ? ( + + idle + + ) : ( + {status} + )}