Skip to content

Conversation

@bybash
Copy link
Collaborator

@bybash bybash commented Dec 22, 2025

https://www.loom.com/share/01130b2036bb4c1e96f51514d18ef306

Summary by CodeRabbit

  • New Features

    • Added blueprint navigation stack with back/forward buttons and keyboard shortcuts
    • Double-click blueprint nodes to open code editor
    • Added open editor button in component actions
    • Auto-focus cursor in half-screen editor mode
    • Enhanced field expansion state coordination across components
  • Bug Fixes

    • Escape key now properly clears editor state and selection

✏️ Tip: You can customize this high-level summary in your review settings.

@bybash bybash requested review from as-flow and madeindjs December 22, 2025 15:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR implements a blueprint page navigation stack with forward/backward navigation, introduces a new BlueprintsNavigationStack UI component with keyboard shortcuts, adds double-click handling on blueprint nodes to open editors or navigate, and coordinates code editor expansion state across components via a shared expandedEditorForComponent reactive property.

Changes

Cohort / File(s) Summary
Core page stack navigation
src/ui/src/core/index.ts, src/ui/src/writerTypes.ts
Introduces page navigation stack with forward/backward navigation methods (navigateInPageStack, canGoBackInPageStack, canGoForwardInPageStack), helper getters (getPreviousPageInPageStack, getNextPageInPageStack), computed pageStackLength, and NavigationStackItem type. Updates setActivePageId to delegate navigation logic.
Blueprint navigation UI
src/ui/src/components/blueprints/BlueprintsNavigationStack.vue, src/ui/src/components/blueprints/BlueprintsRoot.vue
Introduces new BlueprintsNavigationStack component with back/forward buttons, keyboard shortcuts (Ctrl for back, Ctrl+Shift for forward), platform-aware tooltips, and page navigation. Removes onMounted synchronization from BlueprintsRoot and integrates new navigation component.
Blueprint node interactions
src/ui/src/components/blueprints/abstract/BlueprintsNode.vue, src/ui/src/components/blueprints/abstract/BlueprintsNodeActions.vue
Adds double-click handler on blueprint nodes to open code editor or navigate to blueprint root, with toast feedback and analytics tracking. Adds showOpenEditorOption prop and openEditorForComponent handler to node actions, including code editor button in UI.
Editor expansion coordination
src/ui/src/builder/BuilderApp.vue, src/ui/src/builder/BuilderEmbeddedCodeEditor.vue, src/ui/src/builder/builderManager.ts, src/ui/src/builder/settings/BuilderSettingsProperties.vue, src/ui/src/wds/WdsFieldWrapper.vue
Adds expandedEditorForComponent reactive property in BuilderManager to track expanded editor state. Clears state on Escape in BuilderApp, auto-focuses editor in BuilderEmbeddedCodeEditor when variant is "half-screen", coordinates expansion state in BuilderSettingsProperties, and adds shouldExpand prop and watcher in WdsFieldWrapper for external expansion control.
Blueprint creation
src/ui/src/builder/sidebar/BuilderSidebarComponentTree.vue
Introduces getNewBlueprintKey() helper to generate unique BLUEPRINT_ keys. Updates addBlueprint() to use generated key and passes it to createAndInsertComponent. Reorders logic to activate page after DOM tick.
Analytics
src/ui/src/composables/useWriterTracking.ts
Extends WriterTrackingEventName union with four new event types: "dbl_click_for_code_editor_opened", "dbl_click_for_value_opened", "button_click_for_code_editor_opened", "dbl_click_for_blueprint_navigated".

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant BlueprintsNav as BlueprintsNavigationStack
    participant Core
    participant BlueprintsRoot
    participant BuilderManager
    participant DOM as DOM/Component Tree

    User->>BlueprintsNav: Click forward/backward or<br/>keyboard shortcut (Ctrl+Shift)
    BlueprintsNav->>Core: navigateInPageStack("forward"|"backward")
    activate Core
    Core->>Core: Update pageStack index
    Core->>Core: Set activePageId to new page
    deactivate Core
    Core-->>BlueprintsNav: Updated activePageId
    BlueprintsNav->>BuilderManager: clearSelection()
    BuilderManager-->>DOM: Clear selected components
    BlueprintsNav->>DOM: Update displayed blueprint
Loading
sequenceDiagram
    actor User
    participant BlueprintsNode
    participant BuilderManager
    participant BuilderSettingsProperties as BuilderSettingsProperties<br/>(Field Editor)
    participant Editor as BuilderEmbeddedCodeEditor

    User->>BlueprintsNode: Double-click on code component
    BlueprintsNode->>BlueprintsNode: Check if isCodeComponent
    BlueprintsNode->>BuilderManager: Set expandedEditorForComponent<br/>to component.id
    BuilderManager-->>BuilderSettingsProperties: Notify expandedEditorForComponent change
    activate BuilderSettingsProperties
    BuilderSettingsProperties->>BuilderSettingsProperties: Compute isFieldExpanded=true
    BuilderSettingsProperties->>BuilderSettingsProperties: Trigger expansion via :should-expand
    deactivate BuilderSettingsProperties
    BuilderSettingsProperties->>Editor: Mount/show editor
    Editor->>Editor: Auto-focus & move cursor<br/>to last line (half-screen)
    Editor-->>User: Display focused editor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • core/index.ts: Page stack management logic with initialization, forward/backward navigation, and predicate helpers—requires understanding state flow and edge cases.
  • BlueprintsNavigationStack.vue: Multiple computed properties, platform detection, keyboard shortcut handling with global listener lifecycle—verify correct Mac/Windows detection and shortcut routing.
  • expandedEditorForComponent coordination: Spread across five files (BuilderApp, BuilderEmbeddedCodeEditor, builderManager, BuilderSettingsProperties, WdsFieldWrapper)—verify state consistency and clearing behavior across all integration points.
  • getNewBlueprintKey() logic: Key generation and assignment in sidebar; verify uniqueness and ordering behavior.
  • Double-click and keyboard event handlers: Verify event delegation, propagation, and interaction with existing handlers.

Possibly related PRs

Suggested reviewers

  • UladzislauK-Writer
  • madeindjs

Poem

A stack of pages, back and forth we hop, 🐰
Double-click to code, the editors pop!
With blueprints dancing and keyboard shortcuts sung,
Navigation flows smooth—the journey's begun! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: BP Navigation Stack AB-897' clearly describes the main feature being added—a blueprint navigation stack implementation referenced by issue AB-897.
Linked Issues check ✅ Passed The PR implements a comprehensive navigation stack feature with forward/backward navigation capabilities, page stack management, keyboard shortcuts, and UI components that align with the AB-897 requirements.
Out of Scope Changes check ✅ Passed All changes are scoped to the navigation stack feature: new navigation components, page stack logic, editor expansion state management, and related UX enhancements are all directly supporting the main objective.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pullrequest
Copy link

pullrequest bot commented Dec 22, 2025

HackerOne Code Security Review

🟢 Scan Complete: 7 Issue(s)
🟢 Validation Complete: Any Issues detected were validated by one of our engineers. None were determined to require immediate action.

Here's how the code changes were interpreted and info about the tools used for scanning.

📖 Summary of Changes The changes primarily focus on enhancing the builder and blueprints navigation experience. Key modifications include adding an expanded editor tracking mechanism, implementing a navigation stack for blueprints, and introducing new interaction methods like double-click navigation and keyboard shortcuts. The updates improve component management, editor interactions, and provide more flexible navigation between different views and components.
File Summary
src/ui/src/builder/BuilderApp.vue Added a new line in the handleKeydown function to reset the expanded editor for a component when the Escape key is pressed, alongside the existing selection reset.
src/ui/src/builder/BuilderEmbeddedCodeEditor.vue Added logic in onMounted to focus the editor and set cursor position to the last line when the variant is "half-screen", providing additional behavior for modal-like interactions.
src/ui/src/builder/builderManager.ts Added a new ref expandedEditorForComponent to the builder object, which tracks the currently expanded component editor. No other significant changes were observed.
src/ui/src/builder/settings/BuilderSettingsProperties.vue Added a new computed property isFieldExpanded and modified handleExpand and handleShrink methods to manage the expanded state of the editor for the selected component using ssbm.expandedEditorForComponent.
src/ui/src/builder/sidebar/BuilderSidebarComponentTree.vue Added a new function getNewBlueprintKey() to generate unique blueprint keys and modified addBlueprint() to use this function when creating new blueprints, ensuring sequential and unique blueprint identifiers.
src/ui/src/components/blueprints/BlueprintsNavigationStack.vue A new Vue component file was added, implementing a BlueprintsNavigationStack with navigation buttons, blueprint display, and keyboard shortcut handling for moving between blueprints.
src/ui/src/components/blueprints/BlueprintsRoot.vue The changes include adding a BlueprintsNavigationStack component to the template, removing the onMounted lifecycle hook, and introducing new CSS classes for a title container with styling for positioning, typography, and visual appearance.
src/ui/src/components/blueprints/abstract/BlueprintsNode.vue Added a double-click event handler to the node, enabling navigation to code editors or related blueprints. Also added a new prop to BlueprintsNodeActions to show an open editor option for code components.
src/ui/src/components/blueprints/abstract/BlueprintsNodeActions.vue Added a new prop showOpenEditorOption and a new method openEditorForComponent to open the code editor for a component, with a corresponding new button that appears conditionally based on the prop's value.
src/ui/src/composables/useWriterTracking.ts Added four new tracking event names to the WriterTrackingEventName type, including events related to double-clicking and button interactions for code editors and blueprints.
src/ui/src/core/index.ts Added a new pageStack ref and several navigation-related functions to manage page navigation, including methods to go forward and backward in the page stack, check navigation possibilities, and retrieve page information.
src/ui/src/wds/WdsFieldWrapper.vue Added a new prop shouldExpand and implemented a watcher to control modal expansion externally. Imported watch from Vue and modified the script setup to support external expansion control.
src/ui/src/writerTypes.ts The file has been updated to include a new type definition for NavigationStackItem, which represents a navigation stack with a component ID and a key string.
ℹ️ Issues Detected

NOTE: These may not require action!

Below are unvalidated results from the Analysis Tools that ran during the latest scan for transparency. We investigate each of these for accuracy and relevance before surfacing them as a potential problem.

How will I know if something is a problem?
When validation completes, any concerns that warrant attention prior to merge will be posted as inline comments. These will show up in 2 ways:

  • Expert review (most cases): Issues will be posted by experts who manually reviewed and validated them. These are real HackerOne engineers (not bots) reviewing through an integrated IDE-like tool. You can communicate with them like any other reviewer. They'll stay assigned and get notified with commit & comment updates.
  • Automatically: In cases where our validation checks have highest confidence the problem is legitimate and urgent. These will include a description of contextual reasoning why & actionable next steps.
File & Line Issue
src/ui/src/builder/settings/BuilderSettingsProperties.vue Line 294 The code introduces a new feature that automatically expands fields based on a component ID stored in ssbm.expandedEditorForComponent.value. This creates a potential XSS vulnerability if the component ID is user-controlled and not properly sanitized before being used in the UI. The component ID is directly used in the computed property isFieldExpanded and modified in handleExpand and handleShrink functions without validation.
src/ui/src/builder/BuilderEmbeddedCodeEditor.vue Line 119 The code adds functionality to automatically focus the editor and position the cursor at the end of the content when in half-screen mode. This could lead to unexpected behavior for users if they're not expecting the editor to grab focus, potentially disrupting their workflow or causing them to inadvertently modify code.
src/ui/src/core/index.ts Line 955 The getComponentAndNavigate function doesn't properly validate the component ID before using it. On line 955, it calls getComponentById(pageId) but doesn't check if the component exists before accessing its properties on lines 964 and 974. This could lead to a TypeError if the component doesn't exist, potentially causing application crashes or unexpected behavior.
src/ui/src/components/blueprints/abstract/BlueprintsNode.vue Line 445 The handleDoubleClick function doesn't properly validate or sanitize the blueprint key before using it to find and navigate to a blueprint. If the blueprint key contains malicious content, it could potentially lead to unexpected behavior when used in the pushToast message or when attempting to navigate.
src/ui/src/builder/BuilderApp.vue Line 273 The added line 273 sets ssbm.expandedEditorForComponent.value = null when the Escape key is pressed. This could potentially lead to a null reference vulnerability if the expandedEditorForComponent property is not properly initialized or if other parts of the code don't handle null values correctly. Without proper null checks in code that uses this property, it could cause runtime errors or unexpected behavior.
src/ui/src/builder/builderManager.ts Line 438 The code introduces a new reactive reference 'expandedEditorForComponent' that can be set to any string value. This could potentially allow for XSS attacks if user-controlled input is stored in this reference and later rendered in a component without proper sanitization. The value could be manipulated to include malicious script content.
src/ui/src/builder/builderManager.ts Line 422 Using find() is preferred to accessing the first result of a filtered array. NOTE: find() is not supported in IE browsers.
🧰 Analysis tools

⏱️ Latest scan covered changes up to commit 8f20fe9 (latest)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/ui/src/wds/WdsFieldWrapper.vue (1)

112-120: Consider handling the shrink case.

The watcher only triggers expansion when shouldExpand becomes true. If shouldExpand is externally set to false while the field is expanded, the UI won't collapse. Consider whether bidirectional synchronization is needed.

🔎 Optional enhancement for bidirectional sync
 // Watch for external control of expansion
 watch(
 	() => props.shouldExpand,
 	(newVal) => {
 		if (newVal && !isExpanded.value) {
 			handleExpansion();
+		} else if (!newVal && isExpanded.value) {
+			isExpanded.value = false;
+			emits("shrink");
 		}
 	},
 );
src/ui/src/builder/sidebar/BuilderSidebarComponentTree.vue (1)

179-191: Unusual fallback value in key generation.

The fallback calculation uses allBlueprints.value.length + 10 when no existing BLUEPRINT_ keys are found. The + 10 offset seems arbitrary. Consider whether this should simply be 1 to start the sequence, or if there's a specific reason for the offset.

🔎 Proposed simplification
 function getNewBlueprintKey() {
 	const existingKeys = allBlueprints.value
 		.map((bp) => bp.content?.key)
 		.filter((key) => key?.startsWith("BLUEPRINT_"))
 		.map((key) => Number.parseInt(key.replace("BLUEPRINT_", ""), 10))
 		.filter((num) => !Number.isNaN(num));
 
 	const maxIndex =
 		existingKeys.length > 0
 			? Math.max(...existingKeys)
-			: allBlueprints.value.length + 10;
+			: 0;
 	return `BLUEPRINT_${maxIndex + 1}`;
 }
src/ui/src/components/blueprints/abstract/BlueprintsNode.vue (1)

182-184: Consider expanding isCodeComponent to match handleDoubleClick conditions.

The isCodeComponent computed only checks for blueprints_code, but handleDoubleClick also handles blueprints_setstate, blueprints_returnvalue, and blueprints_logmessage for editor expansion. This means the "open editor" action button (controlled by showOpenEditorOption) won't appear for those other component types, which may be inconsistent with the double-click behavior.

🔎 Proposed fix to align the computed with handleDoubleClick logic
 const isCodeComponent = computed(() => {
-	return component.value?.type === "blueprints_code";
+	return (
+		component.value?.type === "blueprints_code" ||
+		component.value?.type === "blueprints_setstate" ||
+		component.value?.type === "blueprints_returnvalue" ||
+		component.value?.type === "blueprints_logmessage"
+	);
 });
src/ui/src/core/index.ts (1)

948-1003: Inconsistent handling of activePageId in navigateInPageStack warrants refactoring.

The function updates activePageId directly when pageId === null (lines 993-996 and 1000) but delegates to the caller when pageId !== null. This asymmetry is mitigated currently because getComponentAndNavigate is only called from setActivePageId, which handles the update sequentially (line 915). However, the inconsistent API design creates maintenance risk—consider refactoring to always or never update activePageId within the function to make the behavior explicit and prevent future misuse.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5ee36de and 8f20fe9.

📒 Files selected for processing (13)
  • src/ui/src/builder/BuilderApp.vue
  • src/ui/src/builder/BuilderEmbeddedCodeEditor.vue
  • src/ui/src/builder/builderManager.ts
  • src/ui/src/builder/settings/BuilderSettingsProperties.vue
  • src/ui/src/builder/sidebar/BuilderSidebarComponentTree.vue
  • src/ui/src/components/blueprints/BlueprintsNavigationStack.vue
  • src/ui/src/components/blueprints/BlueprintsRoot.vue
  • src/ui/src/components/blueprints/abstract/BlueprintsNode.vue
  • src/ui/src/components/blueprints/abstract/BlueprintsNodeActions.vue
  • src/ui/src/composables/useWriterTracking.ts
  • src/ui/src/core/index.ts
  • src/ui/src/wds/WdsFieldWrapper.vue
  • src/ui/src/writerTypes.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-05T12:07:52.320Z
Learnt from: VladKrasny
Repo: writer/writer-framework PR: 1059
File: src/ui/src/builder/settings/BuilderFieldsAlign.vue:27-27
Timestamp: 2025-08-05T12:07:52.320Z
Learning: In Vue 3 templates, refs are automatically unwrapped, so when binding a ref to a template property, you should use the ref directly (e.g., `:value="fieldViewModel"`) instead of explicitly accessing the .value property (e.g., `:value="fieldViewModel.value"`). The .value access is only needed in script contexts.

Applied to files:

  • src/ui/src/wds/WdsFieldWrapper.vue
  • src/ui/src/components/blueprints/abstract/BlueprintsNode.vue
📚 Learning: 2025-08-05T12:07:52.320Z
Learnt from: VladKrasny
Repo: writer/writer-framework PR: 1059
File: src/ui/src/builder/settings/BuilderFieldsAlign.vue:27-27
Timestamp: 2025-08-05T12:07:52.320Z
Learning: In Vue 3 templates, refs are automatically unwrapped, so when binding a ref to a template property, you can use the ref directly (e.g., `:value="fieldViewModel"`) instead of explicitly accessing the .value property (e.g., `:value="fieldViewModel.value"`).

Applied to files:

  • src/ui/src/wds/WdsFieldWrapper.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: tests (chromium)
  • GitHub Check: tests (webkit)
  • GitHub Check: tests (firefox)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.13)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.9)
🔇 Additional comments (30)
src/ui/src/writerTypes.ts (1)

282-285: LGTM! Clean type definition for navigation stack.

The new NavigationStackItem type is well-defined with appropriate typing. Using Component["id"] maintains consistency with existing component references throughout the codebase.

src/ui/src/wds/WdsFieldWrapper.vue (2)

4-6: LGTM! Template formatting corrected.

The WdsModal usage has been properly formatted to ensure the slot content is correctly placed inside the modal body.


84-97: LGTM! External expansion control added.

The new shouldExpand prop enables external control of field expansion, which integrates well with the shared expandedEditorForComponent state introduced elsewhere in this PR.

src/ui/src/builder/sidebar/BuilderSidebarComponentTree.vue (1)

157-177: LGTM! Blueprint key generation integrated.

The reordering of activation after nextTick is good practice, ensuring DOM/state updates complete before setting the active page. The key generation and assignment logic is clean.

src/ui/src/builder/BuilderApp.vue (1)

270-275: LGTM! Escape key now clears expanded editor state.

Adding the expanded editor state clearing on Escape keydown provides consistent UX, ensuring both selection and expanded editors are cleared together.

src/ui/src/components/blueprints/abstract/BlueprintsNodeActions.vue (3)

2-2: LGTM! Imports and prop for new editor button.

The nextTick import and showOpenEditorOption prop are correctly added to support the new code editor button functionality.

Also applies to: 25-25


43-48: LGTM! Code editor expansion handler.

The openEditorForComponent function correctly coordinates selection, DOM updates via nextTick, editor expansion, and analytics tracking. The flow aligns well with the shared expandedEditorForComponent state management introduced in this PR.


107-117: LGTM! New code editor button.

The button is properly guarded by showOpenEditorOption, includes appropriate data attributes for tooltips and unselectability, and uses the correct icon.

src/ui/src/composables/useWriterTracking.ts (1)

45-48: LGTM! Tracking events expanded.

The new event literals correctly extend the tracking system to support the navigation stack and editor expansion interactions introduced in this PR.

src/ui/src/components/blueprints/BlueprintsRoot.vue (3)

3-3: LGTM! Navigation stack component integrated.

The BlueprintsNavigationStack component replaces the automatic page synchronization logic, providing explicit navigation controls with forward/backward navigation as described in the PR objectives.

Also applies to: 30-32


64-90: LGTM! Title container styling added.

The new CSS blocks provide appropriate styling for the navigation stack title area with proper positioning, spacing, and visual separation.


30-30: Initial page state is properly handled through computed properties.

The displayedBlueprintId computed property in both BlueprintsRoot and BlueprintsNavigationStack reactively determines which blueprint to display based on the active page ID. Vue's reactivity system automatically evaluates this property on mount and re-evaluates whenever the underlying state changes, eliminating the need for explicit onMounted synchronization logic. The navigation stack's onMounted hook handles only keyboard shortcuts.

Likely an incorrect or invalid review comment.

src/ui/src/builder/BuilderEmbeddedCodeEditor.vue (1)

119-130: Monaco Editor API methods are available and correctly implemented.

The methods getLineCount() and getLineLastNonWhitespaceColumn() are standard ITextModel API methods available in all supported Monaco Editor versions. The auto-focus and cursor positioning enhancement correctly uses these methods to place the cursor at the last non-whitespace position of the final line when the modal expands.

src/ui/src/builder/builderManager.ts (2)

417-428: LGTM - Inline refactoring preserves original logic.

The collapsed expressions for runId extraction and isActive determination are cleaner while maintaining the same behavior. The optional chaining and Boolean coercion are appropriate.


438-438: LGTM - New shared state for editor expansion.

The expandedEditorForComponent ref provides a single source of truth for tracking which component's editor is expanded, enabling coordination across UI components.

src/ui/src/builder/settings/BuilderSettingsProperties.vue (3)

292-296: LGTM - Computed property for expansion state coordination.

The isFieldExpanded computed correctly derives its value from the shared expandedEditorForComponent state, enabling synchronized expansion across components.


93-93: LGTM - External expansion control binding.

The :should-expand binding correctly combines isExpansible check with the new isFieldExpanded state to drive field wrapper expansion from external triggers.


401-409: LGTM - Proper state synchronization in handlers.

The handleExpand and handleShrink functions correctly update the shared expandedEditorForComponent state alongside the local expandedFields Set, maintaining consistency between the global and local expansion tracking.

src/ui/src/core/index.ts (4)

90-90: LGTM - Page stack state initialization.

The pageStack ref correctly initializes as an empty array to hold navigation history.


913-916: LGTM - Delegation to navigation logic.

The setActivePageId now correctly delegates to getComponentAndNavigate before updating the active page, ensuring navigation stack is properly maintained.


926-946: LGTM - Initial stack seeding logic.

The getComponentAndNavigate correctly seeds the navigation stack with the first blueprint when the stack is empty and no active page exists, preventing loss of the starting point in navigation history.


1080-1085: LGTM - Navigation API exposure.

The new navigation helpers are correctly exposed in the core's public API, enabling the navigation stack UI to query and control navigation state.

src/ui/src/components/blueprints/abstract/BlueprintsNode.vue (2)

14-14: LGTM - Double-click event binding.

The @dblclick handler is correctly bound to enable the new double-click interactions for editor expansion and blueprint navigation.


424-460: LGTM - Double-click handler with appropriate UX flows.

The handler correctly differentiates between code-related components (expanding the editor when selected) and blueprints_runblueprint (navigating to the target blueprint with selection and toast feedback). Analytics tracking is appropriately integrated for both paths.

One minor observation: the tracking event uses isCodeComponent.value to distinguish between "code editor" and "value" events, but given the current isCodeComponent definition only covers blueprints_code, the ternary will always resolve to "dbl_click_for_code_editor_opened" when inside the first if block. This is correct but worth noting if isCodeComponent is updated per the earlier suggestion.

src/ui/src/components/blueprints/BlueprintsNavigationStack.vue (6)

1-45: LGTM - Clean navigation UI template.

The template correctly renders navigation controls with proper disabled states, tooltips, and conditional display. The data-writer-unselectable attribute and tooltip placements are appropriately configured.


57-76: LGTM - Blueprint resolution logic.

The displayedBlueprintId correctly falls back to the first blueprint when the active page is not under the blueprints root, and displayedBlueprintKey properly extracts the key from the component content.


94-104: LGTM - Platform detection with graceful fallback.

The isMac detection correctly tries the modern User-Agent Client Hints API first and falls back to parsing the userAgent string for broader compatibility.


106-122: Note: Tooltips depend on buggy helper functions.

The tooltips use getPreviousPageInPageStack and getNextPageInPageStack which currently have bugs in core/index.ts (returning the current page instead of adjacent pages). Once those are fixed, these tooltips will work correctly.


142-162: LGTM - Keyboard shortcut handling.

The keyboard shortcut logic correctly handles both Ctrl+- for back and Ctrl+Shift+- (which produces _ on most keyboards) for forward navigation. The comment about Shift+- producing "_" is helpful.


164-170: LGTM - Proper event listener lifecycle management.

The keyboard event listener is correctly added on mount and removed on unmount, preventing memory leaks.

Comment on lines +78 to +92
const selectedItemKey = computed(() => {
if (wfbm.selection.value.length > 1) {
return null;
}
const { component, definition: def } = useComponentInformation(
wf,
wfbm.firstSelectedId,
);
if (!component.value || component.value.type === "blueprints_blueprint") {
return null;
}
return component.value?.content?.alias || def.value?.name || "Unknown";
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid calling composables inside computed properties.

Calling useComponentInformation inside a computed property is an anti-pattern in Vue 3. Composables should be called at the top level of setup() to ensure proper lifecycle management and reactivity. The current approach will create new reactive objects on every re-evaluation.

🔎 Proposed fix
 const wf = inject(injectionKeys.core);
 const wfbm = inject(injectionKeys.builderManager);

+const { component: selectedComponent, definition: selectedDef } =
+	useComponentInformation(wf, wfbm.firstSelectedId);
+
 const displayedBlueprintId = computed(() => {
 	// ... existing code
 });

 const selectedItemKey = computed(() => {
 	if (wfbm.selection.value.length > 1) {
 		return null;
 	}

-	const { component, definition: def } = useComponentInformation(
-		wf,
-		wfbm.firstSelectedId,
-	);
-	if (!component.value || component.value.type === "blueprints_blueprint") {
+	if (!selectedComponent.value || selectedComponent.value.type === "blueprints_blueprint") {
 		return null;
 	}

-	return component.value?.content?.alias || def.value?.name || "Unknown";
+	return selectedComponent.value?.content?.alias || selectedDef.value?.name || "Unknown";
 });

Based on learnings, in Vue 3 templates, refs are automatically unwrapped, so the .value access is only needed in script contexts as shown here.

🤖 Prompt for AI Agents
In src/ui/src/components/blueprints/BlueprintsNavigationStack.vue around lines
78-92, the computed property calls the composable useComponentInformation inside
it which creates new reactive state on each re-evaluation; move the call to
useComponentInformation to the top level of setup() (before any computed) and
capture its returned refs (component, definition) there, then update
selectedItemKey to read those refs (and wfbm refs) without re-invoking the
composable; also simplify .value usage in template/script contexts as needed so
the computed only reads existing refs and returns the alias/name/Unknown.

Comment on lines 1023 to 1029
function getPreviousPageInPageStack() {
return pageStack.value.find((item) => item.id === activePageId.value);
}

function getNextPageInPageStack() {
return pageStack.value.find((item) => item.id === activePageId.value);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: getPreviousPageInPageStack and getNextPageInPageStack return the current page instead of adjacent pages.

Both functions have identical implementations that return the current active page rather than the previous/next pages in the stack. This will cause the tooltips in BlueprintsNavigationStack to display incorrect information.

🔎 Proposed fix
 function getPreviousPageInPageStack() {
-	return pageStack.value.find((item) => item.id === activePageId.value);
+	const currentIdx = pageStack.value.findIndex(
+		(item) => item.id === activePageId.value,
+	);
+	if (currentIdx > 0) {
+		return pageStack.value[currentIdx - 1];
+	}
+	return undefined;
 }

 function getNextPageInPageStack() {
-	return pageStack.value.find((item) => item.id === activePageId.value);
+	const currentIdx = pageStack.value.findIndex(
+		(item) => item.id === activePageId.value,
+	);
+	if (currentIdx + 1 < pageStack.value.length) {
+		return pageStack.value[currentIdx + 1];
+	}
+	return undefined;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getPreviousPageInPageStack() {
return pageStack.value.find((item) => item.id === activePageId.value);
}
function getNextPageInPageStack() {
return pageStack.value.find((item) => item.id === activePageId.value);
}
function getPreviousPageInPageStack() {
const currentIdx = pageStack.value.findIndex(
(item) => item.id === activePageId.value,
);
if (currentIdx > 0) {
return pageStack.value[currentIdx - 1];
}
return undefined;
}
function getNextPageInPageStack() {
const currentIdx = pageStack.value.findIndex(
(item) => item.id === activePageId.value,
);
if (currentIdx + 1 < pageStack.value.length) {
return pageStack.value[currentIdx + 1];
}
return undefined;
}
🤖 Prompt for AI Agents
In src/ui/src/core/index.ts around lines 1023 to 1029, both
getPreviousPageInPageStack and getNextPageInPageStack currently return the
active page because they use identical find logic; update them to locate the
index of the activePageId in pageStack.value, then return pageStack.value[index
- 1] for previous and pageStack.value[index + 1] for next, with proper boundary
checks (return undefined/null when no adjacent page exists) and avoid using find
by id for the adjacent lookup.

@pullrequest
Copy link

pullrequest bot commented Dec 22, 2025

✅ Graham C reviewed all the included code changes and associated automation findings and determined that there were no immediately actionable security flaws. Note that they will continue to be notified of any new commits or comments and follow up as needed throughout the duration of this pull request's lifecycle.

Image of Graham C Graham C


Reviewed with ❤️ by PullRequest

Comment on lines +1014 to +1020
return (
pageStack.value.findIndex(
(item) => item.id === activePageId.value,
) +
1 <
pageStack.value.length
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (
pageStack.value.findIndex(
(item) => item.id === activePageId.value,
) +
1 <
pageStack.value.length
);
return getNextPageInPageStack() !== undefined;

Comment on lines 45 to 48
| "dbl_click_for_code_editor_opened"
| "dbl_click_for_value_opened"
| "button_click_for_code_editor_opened"
| "dbl_click_for_blueprint_navigated";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be prefixed by blueprint to follow the convention

Suggested change
| "dbl_click_for_code_editor_opened"
| "dbl_click_for_value_opened"
| "button_click_for_code_editor_opened"
| "dbl_click_for_blueprint_navigated";
| "blueprints_dbl_click_for_code_editor_opened"
| "blueprints_dbl_click_for_value_opened"
| "blueprints_button_click_for_code_editor_opened"
| "blueprints_dbl_click_for_blueprint_navigated";

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file become too big. I think it could be beneficial to move this logic into an internal new hook

export function useNavigationStack(activePageId) {
  // ....
  return {
    navigateInPageStack,
    canGoBackInPageStack,
    canGoForwardInPageStack,
    getPreviousPageInPageStack,
    getNextPageInPageStack,
  }
}

and then you instantiate here

const {
  navigateInPageStack,
  canGoBackInPageStack,
  canGoForwardInPageStack,
  getPreviousPageInPageStack,
  getNextPageInPageStack,
} = useNavigationStack()

It'll also help you to add tests

Comment on lines +282 to +285
export type NavigationStackItem = {
id: Component["id"];
key: string;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's an internal type, I think it can stay in src/ui/src/core/index.ts (or in the new hook as proposed)

Comment on lines +991 to +996
} else {
// if pageId is null, navigate to the next page in the stack
if (currentIdx + 1 < pageStack.value.length) {
activePageId.value = pageStack.value[currentIdx + 1].id;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
// if pageId is null, navigate to the next page in the stack
if (currentIdx + 1 < pageStack.value.length) {
activePageId.value = pageStack.value[currentIdx + 1].id;
}
}
} else if (currentIdx + 1 < pageStack.value.length) {
// if pageId is null, navigate to the next page in the stack
activePageId.value = pageStack.value[currentIdx + 1].id;
}

}

function deleteComponent(componentId: Component["id"]) {
delete components.value[componentId];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to update pageStack here if the user deletes the page.

@bybash bybash changed the base branch from dev to feat/ab-879 December 22, 2025 17:51
@pullrequest
Copy link

pullrequest bot commented Dec 22, 2025

PullRequest reviewed the updates made to #1249 up until the latest commit (1da66e3). No further issues were found.

Reviewed by:

Image of Graham C Graham C

@pullrequest
Copy link

pullrequest bot commented Dec 22, 2025

Graham C has submitted feedback.

Image of Graham C Graham C


Reviewed with ❤️ by PullRequest

}
}
onMounted(() => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyboard shortcut handler is registered globally on the window without checking user context. It intercepts Ctrl+- and Ctrl+Shift+- combinations even when users are typing in input fields, textareas, or contenteditable elements. This prevents users from typing these key combinations as part of legitimate text entry, such as when naming blueprints or entering descriptions.

Global keyboard handlers that don't respect input context create accessibility issues and poor user experience. This violates WCAG guidelines on character key shortcuts, which require that shortcuts either don't interfere with text input or can be disabled.

Remediation:

Add context checking at the start of handleKeyboardShortcut (line 140):

function handleKeyboardShortcut(event: KeyboardEvent) {
    const target = event.target as HTMLElement;
    if (target.matches('input, textarea, [contenteditable="true"]')) {
        return;
    }
    // existing code...
}

This allows shortcuts in the main UI while preserving normal keyboard behavior in text entry contexts.

References:

🔺 Bug (Error)

Image of Graham C Graham C

let pendingComponentUpdate = false;

const activePageId = ref<Component["id"] | undefined>();
const pageStack = ref<NavigationStackItem[]>([]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pageStack array has no maximum size limit, allowing unbounded growth during long user sessions. Each navigation to a blueprint adds an entry, and there's no cleanup mechanism. A user navigating through many blueprints could accumulate hundreds of entries, causing memory exhaustion and browser performance degradation.

This violates OWASP guidelines on resource management, which recommend implementing limits on collection sizes to prevent denial of service conditions. While requiring user interaction to exploit, this is realistic in normal application usage where power users work with many blueprints throughout the day.

Remediation:

Implement a maximum stack size (recommended 50-100 entries). After pushing new entries in navigateInPageStack, add:

if (pageStack.value.length > MAX_STACK_SIZE) {
    pageStack.value.shift();
}

This maintains recent navigation history using a FIFO approach while preventing unbounded growth.

References:

🔸 Bug (Warning)

Image of Graham C Graham C

}

function setActivePageId(componentId: Component["id"]) {
getComponentAndNavigate(componentId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function calls getComponentAndNavigate(componentId) before updating activePageId.value on line 915. This causes navigation logic to operate on stale state. When navigateInPageStack calculates the current index by searching for activePageId.value in the stack, it finds the old page ID, not the new one being navigated to. This leads to incorrect decisions about how to modify the stack structure.

Depending on the current stack state, this can cause broken back/forward navigation, duplicate entries, or stack structures that don't reflect actual user navigation paths. The bug produces different incorrect behaviors based on state, making it difficult to debug.

Remediation:

Reorder the function to update state before calling navigation logic:

function setActivePageId(componentId: Component["id"]) {
    activePageId.value = componentId;
    getComponentAndNavigate(componentId);
}

Alternatively, refactor getComponentAndNavigate to accept both old and new page IDs as parameters so it doesn't rely on global state for calculations.

References:

🔸 Bug (Warning)

Image of Graham C Graham C

c.parentId === "blueprints_root",
);

if (firstBp && firstBp.id !== componentId) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code accesses firstBp.content.key without optional chaining, which can cause a TypeError if the content property is undefined or null. While the code verifies firstBp exists, it doesn't check that content or key are present. Other parts of this file use the safer pattern component?.content?.key ?? pageId (see lines 952, 964) but this initialization logic is inconsistent.

If a blueprint component has undefined content during initialization or data migration, this will crash the navigation system and prevent users from using the application. This is particularly problematic because it occurs during session startup.

Remediation:

Change line 938 to use optional chaining with a fallback:

key: firstBp.content?.key ?? firstBp.id

This matches the pattern used elsewhere in the navigation stack implementation and ensures the code gracefully handles missing properties.

References:

🔸 Bug (Warning)

Image of Graham C Graham C

if (currentIdx + 1 < pageStack.value.length) {
pageStack.value.splice(
currentIdx + 1,
pageStack.value.length - currentIdx - 1,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The navigateInPageStack function inconsistently manages activePageId. When called with pageId === null, it directly updates activePageId.value (lines 971, 977). When called with a specific pageId, it only modifies the stack array without updating activePageId, relying on the caller to handle it. This creates an API where the same function sometimes has side effects on global state and sometimes doesn't, depending on parameter values.

This inconsistency makes the code difficult to understand, test, and maintain. When debugging unexpected activePageId values, it's unclear whether the issue is in this function, in setActivePageId, or in their interaction. The mixed responsibility violates the single responsibility principle.

Remediation:

Refactor to make the function consistently update activePageId in all cases, or separate concerns by creating a pure function that calculates stack modifications without side effects. For the first approach, ensure that whenever the stack is modified, activePageId is also updated to match. Alternatively, make navigateInPageStack never update activePageId and have callers handle it explicitly.

References:

🔸 Bug (Warning)

Image of Graham C Graham C

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants