Skip to content

Enhance Properties panel section behavior and persistence - #3787

Draft
jsjgdh wants to merge 7 commits into
GraphiteEditor:masterfrom
jsjgdh:State
Draft

Enhance Properties panel section behavior and persistence#3787
jsjgdh wants to merge 7 commits into
GraphiteEditor:masterfrom
jsjgdh:State

Conversation

@jsjgdh

@jsjgdh jsjgdh commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

@jsjgdh
jsjgdh marked this pull request as draft February 18, 2026 17:23
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @jsjgdh, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the user experience of the properties panel by enabling sections to be collapsed and expanded, with their state being persisted. It also introduces convenient shortcuts for managing the expansion of all sections at once, improving navigation and focus within complex property layouts.

Highlights

  • Properties Panel Section Expansion: Introduced an expanded field to LayoutGroup::Section to control the collapsed/expanded state of sections in the properties panel, allowing for persistence of this state.
  • State Management for Section Expansion: The PropertiesPanelMessageHandler now maintains a HashMap to store the expanded state of individual sections, ensuring their state is remembered across refreshes.
  • New Messages for Expansion Control: Added SetSectionExpanded and SetAllSectionsExpanded messages to manage the expanded state of individual sections and to toggle the state of all sections simultaneously.
  • Frontend Interaction and Shortcuts: The WidgetSection.svelte component was updated to reflect and dispatch expanded state changes. It also includes a new feature to expand/collapse all sections by holding Alt while clicking a section header.
  • Default Expansion for Merge Nodes: Implemented a special case where 'Merge' nodes in the properties panel will default to a collapsed state for better organization.
Changelog
  • editor/src/messages/layout/utility_types/layout_widget.rs
    • Added expanded field to LayoutGroup::Section enum
    • Updated Diffable implementation for LayoutGroup to include the new expanded field
  • editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
    • Added section_expanded HashMap to NodePropertiesContext to pass expanded state to node properties generation
  • editor/src/messages/portfolio/document/node_graph/node_properties.rs
    • Implemented logic to determine the expanded state for each section, defaulting 'Merge' nodes to collapsed
    • Passed the calculated expanded state to the LayoutGroup::Section constructor
  • editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs
    • Introduced SetAllSectionsExpanded message to toggle the expanded state of all sections
    • Added SetSectionExpanded message to control the expanded state of a specific section
  • editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs
    • Imported HashMap for state management
    • Added section_expanded HashMap field to PropertiesPanelMessageHandler to store section states
    • Passed the section_expanded HashMap to NodePropertiesContext during properties collation
    • Implemented handlers for SetAllSectionsExpanded to update all stored states and refresh the panel
    • Implemented handlers for SetSectionExpanded to update a specific section's state and refresh the panel
  • frontend/src/components/widgets/WidgetSection.svelte
    • Updated expanded state to be derived from widgetData.expanded
    • Removed the TODO comment regarding collapsable sections
    • Modified click handler for section headers to dispatch setSectionExpanded for individual sections
    • Added Alt key modifier to the click handler to dispatch setAllSectionsExpanded for all sections
  • frontend/src/messages.ts
    • Updated WidgetSection type definition to include the expanded: boolean property
    • Adjusted createLayoutGroup function to correctly parse the new expanded property for sections
  • frontend/wasm/src/editor_api.rs
    • Exposed setSectionExpanded function to WASM for setting an individual section's expanded state
    • Exposed setAllSectionsExpanded function to WASM for setting the expanded state of all sections
Activity
  • The pull request was initiated by jsjgdh.
  • A discussion regarding these changes took place on Discord with @Keavon.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a great enhancement to the Properties panel by making sections collapsible and persisting their state. The implementation is well-structured, touching both the backend state management and the frontend UI components. The use of Alt+click to expand/collapse all sections is a nice UX improvement.

My main feedback is regarding the implementation of the "expand/collapse all" feature, which currently has a bug where it doesn't affect sections that haven't been individually toggled. I've left detailed comments with suggestions on how to address this by introducing a global expansion state.

Overall, this is a solid contribution that improves user experience. Addressing the issue I've pointed out will make it even more robust.

Comment on lines +22 to +25
#[derive(Debug, Clone, Default, ExtractField)]
pub struct PropertiesPanelMessageHandler {}
pub struct PropertiesPanelMessageHandler {
pub section_expanded: HashMap<u64, bool>,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The current implementation of SetAllSectionsExpanded has a flaw where it only affects sections that have been individually toggled. To fix this, I suggest adding a field to track a global expansion state. I'll leave another comment on the message handlers with more details.

Suggested change
#[derive(Debug, Clone, Default, ExtractField)]
pub struct PropertiesPanelMessageHandler {}
pub struct PropertiesPanelMessageHandler {
pub section_expanded: HashMap<u64, bool>,
}
#[derive(Debug, Clone, Default, ExtractField)]
pub struct PropertiesPanelMessageHandler {
pub section_expanded: HashMap<u64, bool>,
pub all_sections_expanded: Option<bool>,
}

Comment on lines +68 to +77
PropertiesPanelMessage::SetAllSectionsExpanded { expanded } => {
for value in self.section_expanded.values_mut() {
*value = expanded;
}
responses.add(PropertiesPanelMessage::Refresh);
}
PropertiesPanelMessage::SetSectionExpanded { node_id, expanded } => {
self.section_expanded.insert(node_id, expanded);
responses.add(PropertiesPanelMessage::Refresh);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using the new all_sections_expanded field, this updated logic correctly handles the global expand/collapse behavior.

  • SetAllSectionsExpanded now acts as a global override by setting all_sections_expanded and clearing individual states.
  • SetSectionExpanded clears the global override, allowing individual control again.

To complete this change, you'll also need to:

  1. Pass self.all_sections_expanded to NodePropertiesContext in the Refresh handler (this requires adding all_sections_expanded: Option<bool> to NodePropertiesContext).
  2. In generate_node_properties, prioritize all_sections_expanded when determining a section's expanded state.
Suggested change
PropertiesPanelMessage::SetAllSectionsExpanded { expanded } => {
for value in self.section_expanded.values_mut() {
*value = expanded;
}
responses.add(PropertiesPanelMessage::Refresh);
}
PropertiesPanelMessage::SetSectionExpanded { node_id, expanded } => {
self.section_expanded.insert(node_id, expanded);
responses.add(PropertiesPanelMessage::Refresh);
}
PropertiesPanelMessage::SetAllSectionsExpanded { expanded } => {
self.all_sections_expanded = Some(expanded);
self.section_expanded.clear();
responses.add(PropertiesPanelMessage::Refresh);
}
PropertiesPanelMessage::SetSectionExpanded { node_id, expanded } => {
self.all_sections_expanded = None;
self.section_expanded.insert(node_id, expanded);
responses.add(PropertiesPanelMessage::Refresh);
}

Comment thread editor/src/messages/portfolio/document/node_graph/node_properties.rs Outdated
@Keavon
Keavon force-pushed the master branch 4 times, most recently from 3697b7d to 90533e6 Compare March 11, 2026 12:15
@Keavon
Keavon force-pushed the master branch 7 times, most recently from 9b97ab7 to 2e842cb Compare March 19, 2026 11:00
@cubic-dev-ai

cubic-dev-ai Bot commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

@cubic

@jsjgdh I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 13 files

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@Keavon
Keavon force-pushed the master branch 2 times, most recently from f07c79b to 76938eb Compare April 29, 2026 12:16
@Keavon
Keavon force-pushed the master branch 2 times, most recently from 4b7a823 to 847b8e9 Compare May 17, 2026 14:37
@timon-schelling
timon-schelling force-pushed the master branch 2 times, most recently from 15fcaac to d5f0140 Compare May 17, 2026 15:37
@jsjgdh

jsjgdh commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@cubic-dev

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev

@jsjgdh I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

8 issues found across 14 files

Confidence score: 2/5

  • In editor/src/messages/portfolio/document/utility_types/network_interface/types.rs and editor/src/messages/portfolio/document/utility_types/network_interface/template.rs, the collapsed state is not consistently carried through metadata and NodeTemplate::from_parts, so users can lose explicit Properties-panel collapse choices after save/reopen or template split/join cycles — thread collapsed through all serialize/deserialize and join paths.
  • ToggleNodePropertiesSectionExpanded in editor/src/messages/portfolio/document/document_message_handler.rs and SetCollapsed handling in editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs disagree on network path (&[] vs selection_network_path), which can make root section toggles fail in nested selections — align both read/write operations to the same target network path.
  • editor/src/messages/portfolio/document/graph_operation/utility_types.rs appears to fan out per-node Properties panel side effects during bulk SVG import, risking UI churn and heavy graph/autosave traffic on large files — batch or defer collapse/side-effect messages during bulk operations.
  • In editor/src/messages/portfolio/document/document_message_handler.rs, older documents can drop prior collapse preferences because legacy fields are no longer migrated into persistent_metadata.collapsed — keep a compatibility serde field and migrate IDs on load to preserve user state.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/utility_types/network_interface/types.rs">

<violation number="1" location="editor/src/messages/portfolio/document/utility_types/network_interface/types.rs:603">
P1: Explicit Properties-panel collapse choices are lost on persistence, so reopening a document can restore a section to the implementation default instead of the user’s state. Carry `collapsed` through the GDD metadata and template conversion paths, or intentionally persist it in the existing session view metadata.</violation>
</file>

<file name="editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs:2021">
P2: Root Properties sections can fail to expand or collapse when the current selection is inside a nested network: `SetCollapsed` targets `selection_network_path`, even though the document-level toggle explicitly targets the root network. Carry the node's containing network path in `SetCollapsed` (and pass it from each caller) so the mutation and the read use the same target.</violation>
</file>

<file name="editor/src/messages/portfolio/document/document_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/document_message_handler.rs:252">
P2: Opening a document saved by the previous editor drops its Properties-panel collapse preferences. Retaining a legacy serde field and migrating its node IDs into `persistent_metadata.collapsed` during deserialization would preserve existing documents.</violation>

<violation number="2" location="editor/src/messages/portfolio/document/document_message_handler.rs:252">
P3: The `properties_panel_collapsed_sections` field on `NodePropertiesContext` and `PropertiesPanelMessageContext` is now dead code. The collapsed state is read from `network_interface.is_collapsed()` instead (`node_properties.rs:2464`). The field is always passed as `&[]` and never consumed. Remove it from both context structs and all construction sites to avoid confusion.</violation>

<violation number="3" location="editor/src/messages/portfolio/document/document_message_handler.rs:1409">
P2: `ToggleNodePropertiesSectionExpanded` checks the collapsed state in the document network (`&[]`), but the subsequent `SetCollapsed` message will be processed with `selection_network_path` from the `NodeGraphMessageHandler` context, which may point to a nested network. If the user is working inside a nested network (e.g. a group layer), `is_collapsed(&node_id, &[])` won't find the node there and returns `false` (default), so the toggle always computes `collapsed = true` — the section stays collapsed regardless of the previous state, breaking the toggle for nested-network nodes. The read and write must use the same network path.</violation>
</file>

<file name="editor/src/messages/portfolio/document/graph_operation/utility_types.rs">

<violation number="1" location="editor/src/messages/portfolio/document/graph_operation/utility_types.rs:65">
P2: During bulk SVG import this queues a large flood of PropertiesPanelMessage cascades (SetCollapsed + SetLockedOrVisibilitySideEffects → RunDocumentGraph/SendGraph/UpdateLayerPanel/AutoSave/Refresh) for every created layer, since create_layer is invoked once per node. The collapsed section state is only needed for layers the user creates interactively, so guard the emission with `self.import` to skip it on bulk import.</violation>
</file>

<file name="editor/src/messages/portfolio/document/utility_types/network_interface/view.rs">

<violation number="1" location="editor/src/messages/portfolio/document/utility_types/network_interface/view.rs:189">
P3: This compares the implementation name against a hardcoded `"Merge"` literal, even though the same PR adds a public `MERGE_NODE_IDENTIFIER: &str = "Merge"` constant in `document_node_definitions.rs` and the merge node definition now uses it. Reusing that constant here keeps the merge-node identity in one place so a future rename of the node identifier doesn't silently break the default-collapsed behavior for merge nodes.</violation>
</file>

<file name="editor/src/messages/portfolio/document/utility_types/network_interface/template.rs">

<violation number="1" location="editor/src/messages/portfolio/document/utility_types/network_interface/template.rs:193">
P1: The new `collapsed` state is silently lost whenever a node passes through the `NodeTemplate` split/join cycle, defeating the persistence this PR is adding. `NodeTemplate::from_parts` drops the field on join (`collapsed: _`) and `into_parts` unconditionally writes `collapsed: None` on split. `NodeTemplate::normalize_stored_types` round-trips every nested node through these two methods and is called on every document open (`document_migration_upgrades` -> `normalize_stored_types` at document_migration.rs:1134), so any collapse preference the user saves is reset to `None` on the next load. Since `collapsed` is part of `DocumentNodePersistentMetadata` (serde-persisted), it should be carried through the template shape like `pinned`/`locked` rather than dropped.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub collapsed: Option<bool>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Explicit Properties-panel collapse choices are lost on persistence, so reopening a document can restore a section to the implementation default instead of the user’s state. Carry collapsed through the GDD metadata and template conversion paths, or intentionally persist it in the existing session view metadata.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/utility_types/network_interface/types.rs, line 603:

<comment>Explicit Properties-panel collapse choices are lost on persistence, so reopening a document can restore a section to the implementation default instead of the user’s state. Carry `collapsed` through the GDD metadata and template conversion paths, or intentionally persist it in the existing session view metadata.</comment>

<file context>
@@ -599,6 +599,8 @@ pub struct DocumentNodePersistentMetadata {
 	#[serde(default)]
 	pub pinned: bool,
+	#[serde(default)]
+	pub collapsed: Option<bool>,
 	/// Metadata that is specific to either nodes or layers, which are chosen states for displaying as a left-to-right node or bottom-to-top layer.
 	/// All fields in NodeTypePersistentMetadata should automatically be updated by using the network interface API
</file context>

output_names,
locked,
pinned,
collapsed: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: The new collapsed state is silently lost whenever a node passes through the NodeTemplate split/join cycle, defeating the persistence this PR is adding. NodeTemplate::from_parts drops the field on join (collapsed: _) and into_parts unconditionally writes collapsed: None on split. NodeTemplate::normalize_stored_types round-trips every nested node through these two methods and is called on every document open (document_migration_upgrades -> normalize_stored_types at document_migration.rs:1134), so any collapse preference the user saves is reset to None on the next load. Since collapsed is part of DocumentNodePersistentMetadata (serde-persisted), it should be carried through the template shape like pinned/locked rather than dropped.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/utility_types/network_interface/template.rs, line 193:

<comment>The new `collapsed` state is silently lost whenever a node passes through the `NodeTemplate` split/join cycle, defeating the persistence this PR is adding. `NodeTemplate::from_parts` drops the field on join (`collapsed: _`) and `into_parts` unconditionally writes `collapsed: None` on split. `NodeTemplate::normalize_stored_types` round-trips every nested node through these two methods and is called on every document open (`document_migration_upgrades` -> `normalize_stored_types` at document_migration.rs:1134), so any collapse preference the user saves is reset to `None` on the next load. Since `collapsed` is part of `DocumentNodePersistentMetadata` (serde-persisted), it should be carried through the template shape like `pinned`/`locked` rather than dropped.</comment>

<file context>
@@ -189,6 +190,7 @@ impl NodeTemplate {
 			output_names,
 			locked,
 			pinned,
+			collapsed: None,
 			node_type_metadata,
 			network_metadata,
</file context>

network_interface.set_pinned(&node_id, selection_network_path, pinned);
}
NodeGraphMessage::SetCollapsed { node_id, collapsed } => {
network_interface.set_collapsed(&node_id, selection_network_path, collapsed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Root Properties sections can fail to expand or collapse when the current selection is inside a nested network: SetCollapsed targets selection_network_path, even though the document-level toggle explicitly targets the root network. Carry the node's containing network path in SetCollapsed (and pass it from each caller) so the mutation and the read use the same target.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs, line 2021:

<comment>Root Properties sections can fail to expand or collapse when the current selection is inside a nested network: `SetCollapsed` targets `selection_network_path`, even though the document-level toggle explicitly targets the root network. Carry the node's containing network path in `SetCollapsed` (and pass it from each caller) so the mutation and the read use the same target.</comment>

<file context>
@@ -2023,6 +2017,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
 				network_interface.set_pinned(&node_id, selection_network_path, pinned);
 			}
+			NodeGraphMessage::SetCollapsed { node_id, collapsed } => {
+				network_interface.set_collapsed(&node_id, selection_network_path, collapsed);
+			}
 			NodeGraphMessage::SetVisibility { node_id, network_path, visible } => {
</file context>

fonts,
properties_panel_open,
properties_panel_collapsed_sections: &self.properties_panel_collapsed_sections,
properties_panel_collapsed_sections: &[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Opening a document saved by the previous editor drops its Properties-panel collapse preferences. Retaining a legacy serde field and migrating its node IDs into persistent_metadata.collapsed during deserialization would preserve existing documents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 252:

<comment>Opening a document saved by the previous editor drops its Properties-panel collapse preferences. Retaining a legacy serde field and migrating its node IDs into `persistent_metadata.collapsed` during deserialization would preserve existing documents.</comment>

<file context>
@@ -253,7 +249,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
 					fonts,
 					properties_panel_open,
-					properties_panel_collapsed_sections: &self.properties_panel_collapsed_sections,
+					properties_panel_collapsed_sections: &[],
 				};
 				self.properties_panel_message_handler.process_message(message, responses, context);
</file context>

pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();
self.network_interface.insert_node(new_id, new_merge_node, &[]);
self.responses.add(PropertiesPanelMessage::SetSectionExpanded { node_id: new_id.0, expanded: false });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: During bulk SVG import this queues a large flood of PropertiesPanelMessage cascades (SetCollapsed + SetLockedOrVisibilitySideEffects → RunDocumentGraph/SendGraph/UpdateLayerPanel/AutoSave/Refresh) for every created layer, since create_layer is invoked once per node. The collapsed section state is only needed for layers the user creates interactively, so guard the emission with self.import to skip it on bulk import.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/graph_operation/utility_types.rs, line 65:

<comment>During bulk SVG import this queues a large flood of PropertiesPanelMessage cascades (SetCollapsed + SetLockedOrVisibilitySideEffects → RunDocumentGraph/SendGraph/UpdateLayerPanel/AutoSave/Refresh) for every created layer, since create_layer is invoked once per node. The collapsed section state is only needed for layers the user creates interactively, so guard the emission with `self.import` to skip it on bulk import.</comment>

<file context>
@@ -62,6 +62,7 @@ impl<'a> ModifyInputsContext<'a> {
 	pub fn create_layer(&mut self, new_id: NodeId) -> LayerNodeIdentifier {
 		let new_merge_node = resolve_network_node_type("Merge").expect("Merge node").default_node_template();
 		self.network_interface.insert_node(new_id, new_merge_node, &[]);
+		self.responses.add(PropertiesPanelMessage::SetSectionExpanded { node_id: new_id.0, expanded: false });
 		LayerNodeIdentifier::new(new_id, self.network_interface)
 	}
</file context>

self.properties_panel_collapsed_sections.push(node_id);
}
responses.add(PropertiesPanelMessage::Refresh);
let collapsed = !self.network_interface.is_collapsed(&node_id, &[]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: ToggleNodePropertiesSectionExpanded checks the collapsed state in the document network (&[]), but the subsequent SetCollapsed message will be processed with selection_network_path from the NodeGraphMessageHandler context, which may point to a nested network. If the user is working inside a nested network (e.g. a group layer), is_collapsed(&node_id, &[]) won't find the node there and returns false (default), so the toggle always computes collapsed = true — the section stays collapsed regardless of the previous state, breaking the toggle for nested-network nodes. The read and write must use the same network path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 1409:

<comment>`ToggleNodePropertiesSectionExpanded` checks the collapsed state in the document network (`&[]`), but the subsequent `SetCollapsed` message will be processed with `selection_network_path` from the `NodeGraphMessageHandler` context, which may point to a nested network. If the user is working inside a nested network (e.g. a group layer), `is_collapsed(&node_id, &[])` won't find the node there and returns `false` (default), so the toggle always computes `collapsed = true` — the section stays collapsed regardless of the previous state, breaking the toggle for nested-network nodes. The read and write must use the same network path.</comment>

<file context>
@@ -1411,12 +1406,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
-					self.properties_panel_collapsed_sections.push(node_id);
-				}
-				responses.add(PropertiesPanelMessage::Refresh);
+				let collapsed = !self.network_interface.is_collapsed(&node_id, &[]);
+				responses.add(NodeGraphMessage::SetCollapsed { node_id, collapsed });
+				responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects {
</file context>


pub fn is_collapsed(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
let node_metadata = self.node_metadata(node_id)?;
let collapsed = node_metadata.persistent_metadata.collapsed.unwrap_or_else(|| self.implementation_name(node_id) == "Merge");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This compares the implementation name against a hardcoded "Merge" literal, even though the same PR adds a public MERGE_NODE_IDENTIFIER: &str = "Merge" constant in document_node_definitions.rs and the merge node definition now uses it. Reusing that constant here keeps the merge-node identity in one place so a future rename of the node identifier doesn't silently break the default-collapsed behavior for merge nodes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/utility_types/network_interface/view.rs, line 189:

<comment>This compares the implementation name against a hardcoded `"Merge"` literal, even though the same PR adds a public `MERGE_NODE_IDENTIFIER: &str = "Merge"` constant in `document_node_definitions.rs` and the merge node definition now uses it. Reusing that constant here keeps the merge-node identity in one place so a future rename of the node identifier doesn't silently break the default-collapsed behavior for merge nodes.</comment>

<file context>
@@ -184,6 +184,12 @@ impl<'a, 'p> NetworkView<'a, 'p> {
 
+	pub fn is_collapsed(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
+		let node_metadata = self.node_metadata(node_id)?;
+		let collapsed = node_metadata.persistent_metadata.collapsed.unwrap_or_else(|| self.implementation_name(node_id) == "Merge");
+		Ok(collapsed)
+	}
</file context>

fonts,
properties_panel_open,
properties_panel_collapsed_sections: &self.properties_panel_collapsed_sections,
properties_panel_collapsed_sections: &[],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The properties_panel_collapsed_sections field on NodePropertiesContext and PropertiesPanelMessageContext is now dead code. The collapsed state is read from network_interface.is_collapsed() instead (node_properties.rs:2464). The field is always passed as &[] and never consumed. Remove it from both context structs and all construction sites to avoid confusion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 252:

<comment>The `properties_panel_collapsed_sections` field on `NodePropertiesContext` and `PropertiesPanelMessageContext` is now dead code. The collapsed state is read from `network_interface.is_collapsed()` instead (`node_properties.rs:2464`). The field is always passed as `&[]` and never consumed. Remove it from both context structs and all construction sites to avoid confusion.</comment>

<file context>
@@ -253,7 +249,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
 					fonts,
 					properties_panel_open,
-					properties_panel_collapsed_sections: &self.properties_panel_collapsed_sections,
+					properties_panel_collapsed_sections: &[],
 				};
 				self.properties_panel_message_handler.process_message(message, responses, context);
</file context>

@jsjgdh
jsjgdh marked this pull request as ready for review August 12, 2026 22:44

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 existing issues remain and 3 new issues found across 14 files

Confidence score: 3/5

  • In editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs, Properties section expand/collapse mutates persistent metadata without a guaranteed document transaction, so users can make UI state changes that are not undoable and desync expected history behavior — start a document transaction before dispatching SetCollapsed.
  • In editor/src/messages/portfolio/document/graph_operation/utility_types.rs, create_layer now always enqueues PropertiesPanelMessage::SetSectionExpanded, which fans out into NodeGraphMessage::SetCollapsed and side-effect messages that can trigger extra RunDocumentGraph work; this can introduce unexpected graph churn during layer creation — gate or defer this message so it only fires when section state actually changes.
  • In editor/src/messages/portfolio/document/utility_types/network_interface/template.rs, NodeTemplate conversion drops section collapsed state (from_parts discards it and into_parts forces None), so template round-trips can reset panel state and produce confusing UI regressions — preserve and serialize the collapsed value through both conversion directions.
  • In editor/src/messages/portfolio/document/utility_types/network_interface/view.rs and related node-graph files, merge-node checks are split between is_merge() and hardcoded "Merge"/new constant usage, increasing drift risk where behavior diverges across call sites — standardize on one identifier path (prefer is_merge()/shared constant) and remove duplicate string comparisons.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs">

<violation number="1" location="editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs:26">
P3: The new pub const MERGE_NODE_IDENTIFIER is used in only one place, while the identical literal "Merge" remains hardcoded in sibling comparisons (e.g. document_node_derive.rs, view.rs is_merge/is_collapsed, document_migration.rs, graph_modification_utils.rs). Since the constant names the shared Merge network identifier, using it consistently avoids future drift if the display/identifier value ever changes. Consider referencing MERGE_NODE_IDENTIFIER in those checks as well, or dropping the constant if only this one site is intended.</violation>
</file>

<file name="editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs:2027">
P2: Expanding or collapsing a Properties section is not undoable because this handler mutates the persistent node metadata without ensuring a transaction has started. Start a document transaction before dispatching `SetCollapsed` from each direct caller, or provide a transaction-aware collapse command.</violation>
</file>

<file name="editor/src/messages/portfolio/document/utility_types/network_interface/view.rs">

<violation number="1" location="editor/src/messages/portfolio/document/utility_types/network_interface/view.rs:197">
P3: The fallback identity check `self.implementation_name(node_id) == "Merge"` duplicates the canonical `is_merge()` method defined just above in this same impl block, which compares the node's `DefinitionIdentifier` against `DefinitionIdentifier::Network("Merge")`. Using the human-display string for identity is fragile and redundant: it breaks if the Merge node's display name ever differs from its registry key (the type docs note network display names "don't necessarily have to be the same"), and it re-implements logic already centralized in `is_merge()`. Replace with `self.is_merge(node_id)`.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 3 unresolved issues already reported by Cubic.

Re-trigger cubic

network_interface.set_pinned(&node_id, selection_network_path, pinned);
}
NodeGraphMessage::SetCollapsed { node_id, collapsed } => {
network_interface.set_collapsed(&node_id, selection_network_path, collapsed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Expanding or collapsing a Properties section is not undoable because this handler mutates the persistent node metadata without ensuring a transaction has started. Start a document transaction before dispatching SetCollapsed from each direct caller, or provide a transaction-aware collapse command.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs, line 2027:

<comment>Expanding or collapsing a Properties section is not undoable because this handler mutates the persistent node metadata without ensuring a transaction has started. Start a document transaction before dispatching `SetCollapsed` from each direct caller, or provide a transaction-aware collapse command.</comment>

<file context>
@@ -2029,6 +2023,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
 				network_interface.set_pinned(&node_id, selection_network_path, pinned);
 			}
+			NodeGraphMessage::SetCollapsed { node_id, collapsed } => {
+				network_interface.set_collapsed(&node_id, selection_network_path, collapsed);
+			}
 			NodeGraphMessage::SetVisibility { node_id, network_path, visible } => {
</file context>

use graphene_std::*;
use std::collections::{HashMap, VecDeque};

pub const MERGE_NODE_IDENTIFIER: &str = "Merge";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The new pub const MERGE_NODE_IDENTIFIER is used in only one place, while the identical literal "Merge" remains hardcoded in sibling comparisons (e.g. document_node_derive.rs, view.rs is_merge/is_collapsed, document_migration.rs, graph_modification_utils.rs). Since the constant names the shared Merge network identifier, using it consistently avoids future drift if the display/identifier value ever changes. Consider referencing MERGE_NODE_IDENTIFIER in those checks as well, or dropping the constant if only this one site is intended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs, line 26:

<comment>The new pub const MERGE_NODE_IDENTIFIER is used in only one place, while the identical literal "Merge" remains hardcoded in sibling comparisons (e.g. document_node_derive.rs, view.rs is_merge/is_collapsed, document_migration.rs, graph_modification_utils.rs). Since the constant names the shared Merge network identifier, using it consistently avoids future drift if the display/identifier value ever changes. Consider referencing MERGE_NODE_IDENTIFIER in those checks as well, or dropping the constant if only this one site is intended.</comment>

<file context>
@@ -23,6 +23,8 @@ use graphene_std::vector::Vector;
 use graphene_std::*;
 use std::collections::{HashMap, VecDeque};
 
+pub const MERGE_NODE_IDENTIFIER: &str = "Merge";
+
 pub struct NodePropertiesContext<'a> {
</file context>


pub fn is_collapsed(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
let node_metadata = self.node_metadata(node_id)?;
let collapsed = node_metadata.persistent_metadata.collapsed.unwrap_or_else(|| self.implementation_name(node_id) == "Merge");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The fallback identity check self.implementation_name(node_id) == "Merge" duplicates the canonical is_merge() method defined just above in this same impl block, which compares the node's DefinitionIdentifier against DefinitionIdentifier::Network("Merge"). Using the human-display string for identity is fragile and redundant: it breaks if the Merge node's display name ever differs from its registry key (the type docs note network display names "don't necessarily have to be the same"), and it re-implements logic already centralized in is_merge(). Replace with self.is_merge(node_id).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/utility_types/network_interface/view.rs, line 197:

<comment>The fallback identity check `self.implementation_name(node_id) == "Merge"` duplicates the canonical `is_merge()` method defined just above in this same impl block, which compares the node's `DefinitionIdentifier` against `DefinitionIdentifier::Network("Merge")`. Using the human-display string for identity is fragile and redundant: it breaks if the Merge node's display name ever differs from its registry key (the type docs note network display names "don't necessarily have to be the same"), and it re-implements logic already centralized in `is_merge()`. Replace with `self.is_merge(node_id)`.</comment>

<file context>
@@ -192,6 +192,12 @@ impl<'a, 'p> NetworkView<'a, 'p> {
 
+	pub fn is_collapsed(&self, node_id: &NodeId) -> Result<bool, NetworkError> {
+		let node_metadata = self.node_metadata(node_id)?;
+		let collapsed = node_metadata.persistent_metadata.collapsed.unwrap_or_else(|| self.implementation_name(node_id) == "Merge");
+		Ok(collapsed)
+	}
</file context>

@jsjgdh
jsjgdh marked this pull request as draft August 12, 2026 22:58
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.

2 participants