feat(sidebar): surface unreachable nodes in cross-node stack search (#1195)

Per-node fetch failures in useCrossNodeStackSearch were silently
swallowed into []. The user could not tell the difference between
"this node has no matching stack" and "this node timed out or 502'd".

Adds a third return value to the hook: failedNodes: FailedNode[]
where FailedNode is { nodeId, nodeName, reason }. The reason captures
the HTTP status text (e.g. "list returned HTTP 502") or the
underlying Error message ("connect ECONNREFUSED 192.168.x.x:1852")
without leaking node URLs. AbortError from the effect cleanup path
is intentionally excluded - it's expected when the user keeps typing.

Threads the new field through useStackListState and EditorLayout into
StackList. StackList renders a warning chip below the "Other nodes"
header when the array is non-empty:

  ! N nodes unreachable    > (expand)

Click expands the chip to a vertical list of "node: reason" lines.
Hover surfaces the full reasons in the title attribute too, so a
user inspecting at a glance can read them without clicking. The chip
is suppressed entirely when the search yields no failures (no zero
state). Color is the existing warning token, not error - the failure
is recoverable (node may come back) and not a system-wide problem.

GlobalCommandPalette also calls useCrossNodeStackSearch but ignores
the third return value; its destructuring is additive-safe.

Resolves M-2 from the stack-management audit.
This commit is contained in:
Anso
2026-05-24 15:40:25 -04:00
committed by GitHub
parent 07a2e8f0e3
commit 5196f0440e
4 changed files with 88 additions and 12 deletions
+2
View File
@@ -94,6 +94,7 @@ export default function EditorLayout() {
pinned,
isCollapsed, toggleCollapse,
remoteSearchLoading,
remoteSearchFailedNodes,
} = stackListState;
const { nodes, activeNode, setActiveNode } = useNodes();
@@ -390,6 +391,7 @@ export default function EditorLayout() {
buildMenuCtx,
remoteResults,
remoteLoading: remoteSearchLoading,
remoteFailedNodes: remoteSearchFailedNodes,
onSelectRemoteFile: (nodeId, file) => {
const node = nodes.find(n => n.id === nodeId);
if (node) void stackActions.loadFileOnNode(node, file);