Skip to content

Conversation

@marc-romu
Copy link
Member

@marc-romu marc-romu commented Dec 3, 2025

feat(script-tools): add GhJSON-based script tools, merge workflows, and dialog-canvas linking

Description

This PR prepares the 1.2.0 “script tools” feature set, focusing on GhJSON‑based scripting workflows, GhJSON merge utilities, and improved in-canvas UX:

  • GhJSON-based script tools

    • Introduce script_generate and script_edit AI tools that operate purely on GhJSON for Grasshopper script components.
    • All script tools now validate GhJSON via GHJsonAnalyzer.Validate and use ScriptComponentFactory for component construction.
    • Add script_edit_and_replace_on_canvas wrapper tool that chains script_edit and gh_put in one call, reducing token usage and simplifying AI-driven script editing.
    • Extend script tools to support the full set of parameter modifiers:
      • Inputs: dataMapping (Flatten/Graft), reverse, simplify, invert, isPrincipal, required, expression
      • Outputs: dataMapping, reverse, simplify, invert
  • GhJSON helpers and merge workflows

    • Add GhJsonHelpers utility to apply pivots and restore InstanceGuids on deserialized components.
    • Introduce GhJsonMerger to merge two GhJSON GrasshopperDocument instances with:
      • Target‑document priority on GUID conflicts
      • Automatic ID remapping for connections and groups
    • Add gh_merge AI tool and GhMergeComponents Grasshopper component for merging GhJSON documents, exposing merged GhJSON and basic merge statistics.
  • Component replacement mode and canvas-linked messabe boxes

    • Extend GhPutComponents with an “Edit Mode” input to support component replacement based on existing InstanceGuids.
    • Update gh_put to accept an editMode parameter, preserving original InstanceGuid and exact canvas positions when replacing components, with undo support.
    • Add DialogCanvasLink utility and extend StyledMessageDialog APIs with optional linkedInstanceGuid and linkLineColor so dialogs can visually link to Grasshopper components via a bezier line and anchor dot on the canvas.
  • gh_get improvements

    • Add categoryFilter support to gh_get, extending category-based filtering from components to all document objects.
  • Component and UI updates

    • Update AI‑selecting stateful components to use combined attributes: “Select” button above provider badges, with connector line and hover highlights that match the dialog link color.
    • Improve default SmartHopper assistant prompt used by CanvasButton to guide users toward in‑viewport scripting workflows and avoid unnecessary external code blocks or testing patterns.
    • Refresh component icons (McNeel forum, script, new ghmerge, updated ghget / ghput) for better visual consistency.
  • Provider and metrics improvements

    • Register new Claude Opus 4.5 model in the Anthropic provider registry.
    • Enhance OpenRouter provider with structured output via response_format: json_schema / structured_outputs and improved metrics (finish_reason, model).
    • Fix chat UI metrics to aggregate per turn (from user message to next user message) using ConversationSession.GetTurnMetrics, and ensure tool results inherit the correct TurnId.
  • Stability and UX fixes

    • Fix gh_put infinite loop and “object expired during solution” errors when using replacement mode by removing problematic document enable/disable logic and relying on IsolateObject() during cleanup.
    • Ensure WebChatDialog is an owned Rhino tool window (no taskbar entry, stays with Rhino/Grasshopper focus) and plays nicely with confirmation dialogs.
    • Require all properties in script_generate and script_edit schemas to avoid OpenAI structured-output crashes.

Breaking Changes

  • Files that had previous versions of the Get Components or Place Components components might have to replace them with the new ones.
  • All new behaviors (GhJSON merge, component replacement mode, script edit‑and‑replace tooling, dialog-canvas linking) are opt‑in and preserve existing workflows by default.

Testing Done

  • Manual: Generate new C# and Python script components via AIScriptGeneratorComponent using script_generate and validate GhJSON round‑trip with gh_put.
  • Manual: Edit an existing script component via AIScriptGeneratorComponent + script_edit_and_replace_on_canvas and confirm the component is replaced in place with preserved position and stable behavior.
  • Manual: Use GhMergeComponents (and gh_merge) to merge two GhJSON documents and verify:
    - Components, connections, and groups are merged as expected
    - Merge statistics match the visual result
  • Manual: Enable GhPutComponents Edit Mode to replace existing components and confirm:
    - Prompt behavior via StyledMessageDialog is correct
    - Undo restores the previous state
  • Manual: Open message boxes linked to canvas components (e.g., GhPutComponents confirmations) and visually verify dialog-canvas link lines and hover behavior.
  • Manual: Verify gh_get with categoryFilter returns only the expected objects.
  • Manual: Confirm chat UI shows aggregated per‑turn metrics and that tool calls/results appear under the same turn.
  • Automated: Run the solution build and existing test suites (including SmartHopper.Core.Grasshopper.Tests) and ensure they pass.

Checklist

  • This PR is focused on a single feature or bug fix
  • Version in Solution.props was updated, if necessary, and follows semantic versioning
  • CHANGELOG.md has been updated
  • PR title follows Conventional Commits format
  • PR description follows Pull Request Description Template

- Added programming language keywords (Python, C#, VB, IronPython)
- Added code-related terms (programming, code, codex) to improve search visibility
Updated manifest description to be more concise and improved formatting with separators. Changed project URL from GitHub repository to smarthopper.xyz website.
…mponents

Changed AI-selecting stateful components to use AISelectingComponentAttributes instead of SelectingComponentAttributes, rendering both the Select button and provider badges together. The button appears above the provider strip with proper layout spacing and includes hover/click states with a 5-second auto-hide timer for selected object highlights.
…s when not specified in GhJSON

Changed deserialization logic to default the UsingStandardOutputParam property to true when ShowStandardOutput is not present in the GhJSON ComponentState, ensuring script components show the "out" parameter by default rather than hiding it.
…g in script generator

Updated AIScriptGeneratorComponent to output summary for both generate and edit modes, consolidating summary/changesSummary handling. Changed all error messages to use "Information" output parameter for consistency. Updated tool descriptions to clarify that summary should include design decisions, and made summary a required field in script_generate tool schema.
…or and Review components

Updated AIScriptGeneratorComponent and AIScriptReviewComponent to support processing multiple inputs in parallel:

- Changed all outputs from item/list to tree access with one branch per input
- In create mode: processes each prompt as a separate branch
- In edit mode: matches prompts/questions to selected components using DataTreeProcessor.NormalizeBranchLengths for first-first, second-second matching
Added Edit Mode functionality to GhPutComponents component and gh_put AI tool:

- Added "Edit Mode" boolean input parameter to GhPutComponents that defaults to false
- When enabled and GhJSON contains valid instanceGuids matching canvas components, users are prompted via StyledMessageDialog to choose between replacing existing components or creating new ones
- The gh_put AI tool now accepts optional editMode parameter in its schema
Added DialogCanvasLink utility that draws visual connection lines from dialogs to linked Grasshopper components:

- Created DialogCanvasLink class that manages visual connections between dialogs and canvas components using bezier curves with anchor dots, similar to script editor anchors
- StyledMessageDialog methods now accept optional linkedInstanceGuid and linkLineColor parameters to enable canvas linking
- Registered canvas link callback in
…sual jitter

Added bounds caching in SelectingComponentAttributes and AISelectingComponentAttributes to fix visual instability when highlighting selected objects:

- Introduced cachedSelectedBounds dictionary that stores component bounds when hover starts
- Bounds are computed once via CacheSelectedBounds() method and reused during entire hover session
- Cache is cleared when hover ends to ensure fresh positions on next hover
…for dialogs

Enhanced component replacement workflow and dialog positioning:

- gh_put tool now prompts user individually for each component replacement instead of batch confirmation, allowing selective replacement
- Added CenterViewOnComponent method to CanvasAccess that pans canvas to position components at specified horizontal location (0=left, 0.5=center, 1=right), skipping pan if component already in central 2/3 of viewport
…t with document merging

Enhanced gh_put tool to preserve connections when replacing components:

- Capture components with depth=1 connections before replacement using ConnectionGraphUtils.ExpandByDepth
- Merge captured document into incoming document via new GhJsonMerger utility that handles component/connection/group merging with ID remapping and deduplication
- Disable GH_Document during replacement to prevent solution recalculation
…rge component

Added comprehensive GhJSON document merging functionality:

- Created GhJsonMerger utility to merge two GrasshopperDocument instances with target document taking priority on component GUID conflicts and automatic ID remapping for connections and groups
- Introduced gh_merge AI tool that merges two GhJSON strings and returns merged GhJSON with detailed merge statistics (components/connections/groups added and deduplicated)
…omponent replacement

Fixed two critical issues in gh_put tool's replacement mode:

- Removed NewSolution call that caused infinite loop when GhPutComponents blocked with GetAwaiter().GetResult(), which pumps Windows messages and allows re-entrant solution execution
- Added connection cleanup before component removal by calling RemoveAllSources on inputs and RemoveSource on all output recipients,
…ExecuteTool

Enhanced AIToolManager.ExecuteTool with caller assembly signature verification to prevent unauthorized tool execution:

- Added VerifyCallerAssembly method that validates both Authenticode certificate thumbprint and strong-name public key token match between caller and host assembly
- Cached host assembly's certificate thumbprint and public key token in lazy-initialized static fields for performance
…ure verification

Hardened AIToolManager.VerifyCallerAssembly to require proper signing in all scenarios:

- Removed development mode bypasses that allowed unsigned assemblies when host was unsigned
- Now throws SecurityException if host assembly lacks either Authenticode signature or strong-name signature
- Updated Sign-Authenticode.ps1 to sign all SmartHopper*.dll assemblies instead of only specific provider assemblies
Changed Sign-Authenticode.ps1 to sign all SmartHopper*.dll files instead of only provider and infrastructure assemblies. Simplified filtering logic from explicit name matching to wildcard pattern matching.
…ExecuteTool

Enhanced AIToolManager.ExecuteTool with caller assembly signature verification to prevent unauthorized tool execution:

- Added VerifyCallerAssembly method that validates both Authenticode certificate thumbprint and strong-name public key token match between caller and host assembly
- Cached host assembly's certificate thumbprint and public key token in lazy-initialized static fields for performance
…nentBase and improve connection capture logic

Refactored GhPutComponents to use StatefulAsyncComponentBase for proper async execution and state management:

- Changed GhPutComponents from GH_Component to StatefulAsyncComponentBase with GhPutWorker to prevent re-entrancy issues
- Set RunOnlyOnInputChanges to false to allow re-placing same JSON multiple times
- Improved deprecated badge appearance
…in selecting components

Enhanced SelectingComponentBase and CombinedSelectingComponentAttributes to draw a visual connector line from the combined selection center to the Select button during hover:

- Changed highlight color from DodgerBlue to DialogCanvasLink.DefaultLineColor for consistency
- Calculate union of all selected object bounds and draw connector line from center to button center
- Added anchor dots at both ends of connector
…eason/model in metrics

Added structured output support for JsonOutput requests using `response_format: json_schema` and `structured_outputs` flag. Now populates `finish_reason` and `model` fields in AIMetrics for chat completions.
…rasshopper category or subcategory

Added category filtering capability to gh_get tool and GhGetComponents:

- Added "Category Filter" input parameter (index 1) with include/exclude syntax
- Implemented PassesCategoryFilters method in ComponentRetriever to match against both Category and SubCategory fields
- Updated gh_get tool schema to document categoryFilter parameter
…ting" to "NotTested" for all script parameter modification tools
…TurnId propagation

- Fixed chat UI to display total token consumption per turn (from user message to next user message) instead of just the last message's metrics
- Added `GetTurnMetrics(turnId)` method to `ConversationSession` to aggregate metrics from all interactions in a turn (assistant messages, tool calls, and tool results)
- Fixed tool results not inheriting TurnId from their corresponding tool calls in `ToolManager.ExecuteTool`
… to reduce token consumption

Added a convenience wrapper tool that combines script_edit and gh_put in a single call:

- Created script_edit_and_replace_on_canvas tool that internally executes script_edit followed by gh_put with editMode=true
- Changed script_edit category from "Scripting" to "Hidden" since the wrapper is now the preferred interface
- Updated CanvasButton system prompt to document the new workflow using the wrapper tool
…cript_generate and script_edit

Enhanced script_generate and script_edit tools to support all parameter modifiers for both inputs and outputs:

- Added support for dataMapping (Flatten/Graft), reverse, simplify, invert, isPrincipal, required, and expression modifiers for input parameters
- Added support for dataMapping, reverse, simplify, and invert modifiers for output parameters
- Updated tool schemas to document all available
…Rhino focus and stays on top

- Set WebChatDialog as an owned window of Rhino's main Eto window and hide it from the taskbar
- Dialog now follows Rhino/Grasshopper focus and stays on top of Rhino while the application is active
- Fixed issue where confirmation dialogs (e.g., from gh_put in edit mode) would leave the chat window hidden behind other windows after closing
…se instanceGuid and make parameter schemas more explicit

- Changed script_edit_and_replace_on_canvas wrapper to accept instanceGuid instead of ghjson, automatically calling gh_get_by_guid internally before script_edit and gh_put
- Made all optional parameter modifier fields required in JSON schemas for script_generate and script_edit to force explicit specification
- Added guidance text to parameter descriptions
…nnecessary code blocks and external testing patterns

- Added guidance that all scripting happens inside Grasshopper script components, not standalone environments
- Instructed to avoid proposing unit tests or external test projects since validation happens directly in Grasshopper
- Added instruction to tell users to double-click script components instead of pasting full scripts in chat
- Discouraged copying full scripts into the conversation
Copilot AI review requested due to automatic review settings December 3, 2025 17:31
@github-actions
Copy link
Contributor

github-actions bot commented Dec 3, 2025

🏷️ This PR has been automatically assigned to milestone 1.2.0-alpha based on the version in Solution.props.

@marc-romu marc-romu added scope: AI Tools Issues related to the AI tools scope: UI Issues related to the UI features scope: Canvas Assistant Issues related to the Canvas Assistant logic labels Dec 3, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a comprehensive script tools feature set (version 1.2.0) that enables AI-powered script component generation and editing through GhJSON workflows, along with improved canvas interaction and dialog UX enhancements.

  • GhJSON-based script tools: Introduces script_generate and script_edit AI tools that operate on GhJSON for creating and modifying script components, with support for comprehensive parameter modifiers
  • Component replacement workflow: Extends gh_put with edit mode to replace existing components in-place with user confirmation and connection preservation
  • Dialog-canvas linking: Adds visual bezier lines connecting dialogs to related canvas components for improved spatial awareness
  • Infrastructure improvements: Enhances metrics tracking (per-turn aggregation), adds structured output for OpenRouter, registers Claude Opus 4.5, and improves selection component UI

Reviewed changes

Copilot reviewed 74 out of 86 changed files in this pull request and generated 32 comments.

Show a summary per file
File Description
yak-package/manifest.yml Updates package description, URL, and keywords for script tools release
tools/Update-InternalsVisibleTo.ps1 New PowerShell script to update InternalsVisibleTo with public key from signing.snk
src/SmartHopper.Infrastructure/SmartHopper.Infrastructure.csproj Adds InternalsVisibleTo configuration with public key for controlled access
src/SmartHopper.Infrastructure/Dialogs/StyledMessageDialog.cs Changes visibility to internal, adds canvas linking support, implements dynamic sizing
src/SmartHopper.Infrastructure/AITools/ToolManager.cs Propagates TurnId from tool calls to tool results
src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSessionHelpers.cs Adds helpers for non-streaming follow-ups and tool call history updates
src/SmartHopper.Infrastructure/AICall/Sessions/ConversationSession.cs Implements GetTurnMetrics and UpdateToolCallInHistory for streaming accumulation
src/SmartHopper.Infrastructure/AICall/Core/Returns/AIReturn.cs Adds conditional metrics validation based on SkipMetricsValidation flag
src/SmartHopper.Infrastructure/AICall/Core/Requests/AIRequestBase.cs Adds SkipMetricsValidation property for local tools
src/SmartHopper.Providers.OpenRouter/OpenRouterProvider.cs Implements structured output via response_format and improves metrics extraction
src/SmartHopper.Providers.Anthropic/AnthropicProviderModels.cs Registers Claude Opus 4.5 model and updates model ranks
src/SmartHopper.Core/UI/DialogCanvasLink.cs New utility for drawing visual links between dialogs and canvas components
src/SmartHopper.Core/UI/Chat/WebChatObserver.cs Uses GetTurnMetrics for per-turn metric aggregation in chat UI
src/SmartHopper.Core/UI/Chat/WebChatDialog.cs Sets dialog as owned window with no taskbar entry
src/SmartHopper.Core/UI/CanvasButton.cs Updates default system prompt with scripting workflow guidance
src/SmartHopper.Core/ComponentBase/* Enhances selection components with visual overlays and shared rendering logic
src/SmartHopper.Core.Grasshopper/Utils/* Adds CenterViewOnComponent and PassesCategoryFilters utilities
src/SmartHopper.Core.Grasshopper/Serialization/GhJson/* Adds GhJsonHelpers, GhJsonMerger, and extends ScriptComponentFactory
src/SmartHopper.Core.Grasshopper/Serialization/Canvas/ComponentPlacer.cs Adds useExactPositions mode for replacement scenarios
src/SmartHopper.Core.Grasshopper/AITools/script_generate.cs New tool for generating script components as GhJSON
src/SmartHopper.Core.Grasshopper/AITools/script_edit.cs New tool for editing script components via GhJSON with wrapper for canvas replacement
src/SmartHopper.Core.Grasshopper/AITools/gh_put.cs Implements edit mode with component replacement and connection preservation
src/SmartHopper.Core.Grasshopper/AITools/gh_merge.cs New tool for merging two GhJSON documents
src/SmartHopper.Core.Grasshopper/AITools/gh_get.cs Adds categoryFilter support for filtering by Grasshopper category
src/SmartHopper.Core.Grasshopper/AITools/* Adds SkipMetricsValidation to local tools
src/SmartHopper.Components/Script/* New AIScriptGeneratorComponent and AIScriptReviewComponent
src/SmartHopper.Components/SmartHopperAssemblyPriority.cs Registers canvas centering and dialog link callbacks
Files not reviewed (1)
  • src/SmartHopper.Components/Properties/Resources.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 156 to 162
foreach (var msg in aiResult.Messages)
{
if (!string.IsNullOrWhiteSpace(msg?.Message))
{
parts.Add($"{msg.Severity}: {msg.Message}");
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines 210 to 220
foreach (var msg in (JArray)toolResult["messages"])
{
if (msg["severity"]?.ToString() == "Error")
{
var text = msg["message"]?.ToString();
if (!string.IsNullOrWhiteSpace(text))
{
this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, text);
}
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines 223 to 233
foreach (var msg in (JArray)toolResult["messages"])
{
if (msg["severity"]?.ToString() == "Error")
{
var text = msg["message"]?.ToString();
if (!string.IsNullOrWhiteSpace(text))
{
this.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, text);
}
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines 225 to 231
foreach (var obj in selectingComponent.SelectedObjects.OfType<IGH_DocumentObject>())
{
if (obj.Attributes != null)
{
result[obj.InstanceGuid] = obj.Attributes.Bounds;
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines 98 to 104
foreach (var typeStr in typeFilterArray)
{
if (Enum.TryParse<ObjectType>(typeStr.ToString(), true, out var objType))
{
typeFilter.Add(objType);
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines 202 to 209
if (obj is IGH_Component comp)
{
if (comp.Params.Input.Contains(param) || comp.Params.Output.Contains(param))
{
owner = comp;
break;
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

These 'if' statements can be combined.

Copilot uses AI. Check for mistakes.
internal class StyledMessageDialog : Dialog
{
private bool _result;
private Guid _linkedInstanceGuid = Guid.Empty;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Field '_linkedInstanceGuid' can be 'readonly'.

Copilot uses AI. Check for mistakes.
{
private bool _result;
private Guid _linkedInstanceGuid = Guid.Empty;
private System.Drawing.Color? _linkLineColor;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Field '_linkLineColor' can be 'readonly'.

Copilot uses AI. Check for mistakes.
Comment on lines 611 to 618
if (!string.IsNullOrWhiteSpace(turnId))
{
aggregated.Metrics = this._dialog._currentSession?.GetTurnMetrics(turnId) ?? finalAssistant.Metrics;
}
else
{
aggregated.Metrics = finalAssistant.Metrics;
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
$items = Get-ChildItem -Path $Sign -Recurse -Filter "*.dll" |
Where-Object { $_.Name -like "SmartHopper.Providers.*.dll" -or $_.Name -eq "SmartHopper.Infrastructure.dll" }
Write-Host "Signing all SmartHopper assemblies under directory: $Sign"
$items = Get-ChildItem -Path $Sign -Recurse -Filter "SmartHopper*.dll"
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The broadened signing glob in $items = Get-ChildItem -Recurse -Filter "SmartHopper*.dll" risks code-signing unintended or attacker-planted assemblies. If an untrusted DLL matching SmartHopper*.dll is present anywhere under $Sign, this script will sign it, lending trust to malicious code and enabling supply-chain compromise. Restrict signing to an explicit allowlist of known assemblies or a manifest from the build system (e.g., enumerate exact project output names/paths), and validate binaries before signing (e.g., ensure they originate from expected build directories and match expected file names/hashes).

Copilot uses AI. Check for mistakes.
- Replaced foreach loops with LINQ expressions (Where, Select, ToHashSet, etc.) across multiple files for more concise code
- Added explicit assembly allowlist to Sign-Authenticode.ps1 to prevent signing unintended/malicious assemblies
- Added validation to reject non-SmartHopper assemblies when signing explicit DLLs
- Changed floating point comparison in script_edit.cs to use tolerance instead of direct equality check
@marc-romu marc-romu merged commit 952516b into dev Dec 3, 2025
9 checks passed
@github-actions github-actions bot deleted the feature/1.2.0-script-tools branch December 3, 2025 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: AI Tools Issues related to the AI tools scope: Canvas Assistant Issues related to the Canvas Assistant logic scope: UI Issues related to the UI features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants