R8a: restore Hide Other Branches across five node kinds - #171
Merged
Conversation
Chat, code, document, thinking and image node menus each carried a disabled "Hide Other Branches" stub with the tooltip "Branch visibility isn't built yet" since the Qt cutover deleted the legacy scene's toggle_branch_visibility (graphlink_scene.py) without a successor. Legacy's own name is misleading: it never removes anything. It dims every node outside the clicked node's branch - its ancestor chain and everything descended from it - down to a low opacity (0.18, legacy's own BRANCH_DIM_OPACITY, ported unchanged), restoring full opacity on toggle-off. It is a scene-wide toggle: once active, the button reads "Show All Branches" everywhere, and clicking anywhere clears it, regardless of which node's menu the second click came from. computeDimmedNodeIds (SceneCanvas.tsx) ports the legacy algorithm to this app's uniform (source, target) edge model. Legacy told "chat replied by chat" and "chat owns code block" apart with different attribute pairs (parent_node/children vs. parent_content_node); the modern backend models both as one edge type, so this tells them apart by kind instead - a node's branch anchor is itself if chat-kind, else its nearest parent if that is chat-kind. The active branch is computed by walking chat-to-chat edges only, in both directions; a content node attached to any branch member counts as active via its anchor, not by the walk expanding through it. Both the ancestor walk and the descendant search are visited-set- guarded - a deliberate hardening legacy's own equivalent walks lack, since SceneDocument.connect() allows creating a cycle with no validation at all. Applied via React Flow's per-node style.opacity, the same mechanism this file already established for the fade-connections feature, rather than any DOM removal. Dimming is scoped to exactly the five node kinds that carried this menu item in the legacy app - every other kind, including Conversation (whose own file already documents excluding this feature deliberately), is left untouched. Built in two stages: the algorithm and all SceneCanvas state were written and hand-verified directly against 13 graph scenarios (linear chains, sibling forks, deep descendant chains, content nodes on dimmed branches, focusing from a content node itself, orphaned nodes, manually-created cycles) before any other file was touched, then five agents wired the mechanical menu-button change into the five node views in parallel from one shared contract. Three independent reviews followed: one devised its own six adversarial graph scenarios blind to the existing test suite and hand-traced them against the real implementation before cross-checking; one verified cross-file contract correctness (interface fields, prop threading, tsc, full test run); one checked legacy fidelity and test quality. All three came back clean bar one test-coverage gap (a missing menu-closed assertion in one file's active-state test), fixed directly. That same direct verification pass also caught a real lint error the review agents had no visibility into, since it was introduced after their scope was fixed: the original self-heal-on-deleted-origin logic used setState synchronously inside a useEffect, the exact cascading-render anti-pattern react-hooks/set-state-in-effect exists to catch. Replaced with a derived value computed during render - simpler, and fixes a stale node reference within the same render instead of one render later. Verified independently: full backend suite (1033, unaffected), frontend typecheck/lint/tests/build (841 tests, 0 lint errors), and a live browser check against a real 13-node scene with an actual branch fork - clicking Hide Other Branches dropped exactly the sibling branch's five nodes (spanning multiple kinds, including a thinking node attached to that branch) to opacity 0.18, the label flipped to Show All Branches, and toggling off restored all thirteen nodes to opacity 1, with zero console errors throughout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Restores the "Hide Other Branches" menu action across chat, code, document, thinking and image nodes, replacing a disabled placeholder with a working branch-isolation toggle.
Problem
All five node kinds' menus carried the item disabled, with the tooltip "Branch visibility isn't built yet". The Qt cutover deleted the legacy scene's
toggle_branch_visibility(graphlink_scene.py) without porting a successor.Legacy's own name is misleading: it never removes anything. It dims every node outside the clicked node's branch — its ancestor chain and everything descended from it — down to a low opacity, restoring full opacity on toggle-off. It is a scene-wide toggle: once active, the button reads "Show All Branches" everywhere, and clicking anywhere clears it regardless of which node's menu the second click came from.
Changes
computeDimmedNodeIds(SceneCanvas.tsx) ports the legacy algorithm to this app's uniform(source, target)edge model. Legacy told "chat replied by chat" and "chat owns a code block" apart with different attribute pairs; the modern backend models both as one edge type, so this tells them apart by kind instead — a node's branch anchor is itself if chat-kind, else its nearest parent if that is chat-kind. The active branch is the set of chat nodes reachable by walking chat-to-chat edges only, in both directions; a content node attached to any branch member counts as active via its anchor, not by the walk expanding through it.Both the ancestor walk and the descendant search are visited-set-guarded — a deliberate hardening legacy's own equivalent walks lack, since
SceneDocument.connect()permits creating a cycle with no validation. Dimming is applied via React Flow's per-nodestyle.opacity, the same mechanism this file already uses for the fade-connections feature, at0.18— legacy's own constant, ported unchanged.Dimming is scoped to exactly the five node kinds that carried this menu item in the legacy app. Every other kind, including Conversation (whose own file already documents excluding this feature deliberately), is left untouched.
Process
Built in two stages. The algorithm and all
SceneCanvasstate were written and hand-verified against 13 graph scenarios — linear chains, sibling forks, deep descendant chains, content nodes on dimmed branches, focusing from a content node itself, orphaned nodes, manually-created cycles — before any other file was touched. Five agents then wired the mechanical menu-button change into the five node views in parallel from one shared contract.Three independent reviews followed: one devised its own six adversarial graph scenarios blind to the existing test suite and hand-traced them against the implementation before cross-checking against it; one verified cross-file contract correctness; one checked legacy fidelity and test quality. All three came back clean bar one test-coverage gap — a missing menu-closed assertion in one file's active-state test — fixed directly.
That same direct verification pass caught a real lint error outside the reviewed scope: the original self-heal-on-deleted-origin logic called
setStatesynchronously inside auseEffect, the cascading-render anti-patternreact-hooks/set-state-in-effectexists to catch. Replaced with a value derived during render — simpler, and resolves a stale node reference within the same render instead of one render later.Verification
tsc --noEmit/ ESLint / tests / build0.181Related
This closes out the four-item deferred-menu-item cleanup started with Key Takeaway/Explainer Note (#169) and Open Document View (#170).